不用 LangChain,不用 CrewAI,不用任何框架。只用 Python 标准库 + OpenAI SDK,从聊天机器人一路进化到能读文件、写代码、执行命令的自主编程助手。本文带你拆解 AI Agent 的核心架构,理解 Claude Code、Cursor、Copilot 背后的同一套设计模式。


OwnitAgent基于本文的逻辑和代码实现,增加记忆系统
项目地址:https://github.com/nangongchengfeng/OwnitAgent.git
在这里插入图片描述

为什么你应该从零写一个 AI Agent

市面上的 AI 编程工具琳琅满目——Claude Code、Cursor Agent、GitHub Copilot Workspace——它们看起来像是黑魔法,但剥开外壳,底层跑的是同一套架构。用框架当然快,但你会错过理解"为什么"的机会。

当你亲手实现一遍 Agentic Loop,你会明白:

  • AI 到底是怎么"自主"做多步决策的
  • Function Calling 的消息协议为什么长那样
  • 工具定义的好坏如何直接影响 Agent 的行为质量
  • 安全边界在哪里——AI 从不真正执行任何操作

更重要的是,理解架构之后,你可以定制它、扩展它、在同样的基础上造出全新的东西。这就是本文的目标。


聊天机器人 vs Agent:差一个工具系统

先搞清楚一个根本问题:聊天机器人和 Agent 到底差在哪?

🦾 AI Agent

👤 你:帮我写个 HTTP 服务

AI:列出项目结构

AI:创建 app.py

AI:写入代码

AI:安装依赖

AI:启动服务

AI:验证响应 → 报告结果 ✅

🤖 聊天机器人

👤 你:帮我写个 HTTP 服务

AI:好的,这是代码...

👤 你:复制、粘贴、创建文件、pip install、运行、调试...

区别就在于工具调用(Function Calling)。Agent 拥有工具——读文件、写文件、执行命令——并且自主决定何时用、怎么用。聊天机器人只能"说",Agent 能"做"。


核心架构:三件事

现代 AI Agent 的整个架构归结为三个组件:

🏗️ AI Agent 核心架构

推理决策

能力清单

选择工具

调用 API

① LLM API
支持 Function Calling
(大脑)

③ Agentic Loop
编排思考与行动的循环
(神经系统)

② 工具定义
JSON Schema 描述可用能力
(双手)

任何一个生产级 AI 编程工具,拆开来看都是这三样东西。下面我们逐个击破。


Agentic Loop:Agent 的心脏

Agentic Loop 是让 AI "自主"的核心机制。它不是一次性问答,而是一个思考→行动→观察→再思考的循环:

📝 文本回复

🔧 工具调用

👤 用户发送消息

🧠 LLM 接收消息 + 工具定义

LLM 决策:
回复文本还是调用工具?

✅ 返回给用户
循环结束

⚙️ 你的代码执行工具

📤 将结果发回 LLM

一个真实的执行过程可能是这样的:

💻 Shell 📁 文件系统 🧠 MagicCode 💻 Shell 📁 文件系统 🧠 MagicCode 第 1 轮 第 2 轮:分析代码,定位问题 第 3 轮:验证修改 第 4 轮:任务完成 👤 用户 "给 main.py 加上错误处理" list_files(".") 项目结构 read_file("main.py") 文件内容(带行号) edit_file("main.py", old, new) 编辑完成 run_command("pytest") 测试全部通过 ✅ "全部通过,这是改动总结..." 👤 用户

关键洞察:AI 计划、行动、观察、调整——全程自主。这就是 “Agentic” 的含义。


Function Calling 的工作原理

OpenAI 的 Function Calling(Anthropic 叫 Tool Use)机制是 Agent 能"做事"的基础。它的工作流程分四步:

🔧 工具执行 ☁️ LLM API 👨‍💻 开发者代码 🔧 工具执行 ☁️ LLM API 👨‍💻 开发者代码 第一步:定义工具 第二步:LLM 选择 第三步:代码执行 第四步:结果返回 发送工具定义(JSON Schema) + 用户消息 回复:调用 write_file 参数 {"path":"hello.py","content":"..."} execute_tool("write_file", params) "Written to hello.py (20 chars)" 发送 role="tool" 消息 基于结果继续推理...

工具定义示例

工具定义是一份 JSON Schema,告诉 LLM 你有哪些能力可用:

{
    "type": "function",
    "function": {
        "name": "read_file",
        "description": "读取文件内容,返回带行号的文本",
        "parameters": {
            "type": "object",
            "properties": {
                "path": {
                    "type": "string",
                    "description": "要读取的文件路径"
                }
            },
            "required": ["path"]
        }
    }
}

工具定义的质量直接影响 Agent 的行为。 三条原则:

原则 好的做法 差的做法
直觉化命名 read_file rf
具体描述 “读取文件内容,返回带行号的文本” “读文件”
精确 Schema 明确 required、类型、默认值 模糊的参数定义

这是整个架构的安全基石:AI 从不亲自执行任何操作。 它只输出"我想调用 write_file,参数是 {"path": "hello.py", "content": "..."}"。实际的文件写入、命令执行全部由你的 Python 代码完成。执行边界完全在你掌控之中。


消息协议:调试的命门

理解对话历史的消息结构,能帮你省下大量调试时间:

☁️ LLM API 📜 对话历史 👤 用户 ☁️ LLM API 📜 对话历史 👤 用户 消息协议流转 你的代码执行 write_file role="user" "写个 hello world" 发送完整 history 数组 role="assistant" content="我来创建文件" tool_calls=[{id:"call_abc", name:"write_file", args:{...}}] role="tool" tool_call_id="call_abc" content="Written to hello.py" 再次发送完整 history(含工具结果) role="assistant" content="文件已创建!"

具体的数据结构如下:

history = [
    # 系统消息:定义角色和行为规则
    {"role": "system", "content": "You are MagicCode..."},

    # 用户消息
    {"role": "user", "content": "写个 hello world"},

    # AI 回复:可能包含工具调用
    {
        "role": "assistant",
        "content": "我来创建文件。",
        "tool_calls": [{
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "write_file",
                "arguments": '{"path":"hello.py","content":"print(\"hello\")"}'
            }
        }]
    },

    # 工具结果:通过 tool_call_id 精确匹配
    {
        "role": "tool",
        "tool_call_id": "call_abc123",
        "content": "Written to hello.py (20 chars)"
    },

    # AI 继续推理...
]

两个容易踩坑的细节:

  1. 工具结果用 role: "tool",不是 role: "user" 模型对两者的处理方式不同——它知道 tool 消息来自工具执行,而非人类输入。
  2. tool_call_id 必须精确匹配。 每个工具结果必须引用对应工具调用的 id,ID 不匹配 API 直接报错。

动手实现:四个版本逐步进化

我们分四个版本构建一个名为 MagicCode 的终端 AI 编程 Agent。每个版本都能独立运行,随时可以停下来,手里都有一个可用的东西。

V1 基础对话 20 行代码 Chat Completions API 能说不能做 V2 流式输出 30 行代码 Token 流式传输 打字机效果 V3 富文本终端 35 行代码 Rich Markdown 渲染 终端也能好看 V4 完整 Agent 250 行代码 Agentic Loop + Function Calling 自主编程助手 MagicCode 版本进化路线

V1:20 行打地基

先跑通最基础的对话循环——验证 API 能通,对话历史能正确维护:

from openai import OpenAI

client = OpenAI()
history = [
    {"role": "system", "content": "You are MagicCode, a terminal AI coding assistant."}
]

print("MagicCode v1 — 输入 'exit' 退出")
while True:
    user_input = input("\nYou > ")
    if user_input.strip().lower() in ("exit", "quit"):
        break

    history.append({"role": "user", "content": user_input})

    response = client.chat.completions.create(
        model="gpt-4o",
        messages=history,
    )

    reply = response.choices[0].message.content
    history.append({"role": "assistant", "content": reply})
    print(f"\n{reply}")

核心概念history 列表就是对话记忆。没有什么神奇的持久化机制——就是一个不断增长的数组。这也是为什么长对话会撞 Token 限制、费用越来越高的原因。

V2:流式输出——打字机效果

V1 的问题是:生成长回复时,你得盯着空白终端干等。流式输出让 Token 生成一个、显示一个:

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=history,
    stream=True,  # 关键改动
)

full_reply = ""
for chunk in stream:
    delta = chunk.choices[0].delta.content
    if delta:
        print(delta, end="", flush=True)
        full_reply += delta

flush=True 比你想象的更重要——没有它,Python 会缓冲输出,你看到的不是流畅的逐字符显示,而是一阵一阵的文字突然蹦出来。

V3:Rich 富文本终端

终端不一定非得丑。用 rich 库实现 Markdown 渲染、语法高亮和实时面板:

from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from rich.live import Live

console = Console()

with Live(console=console, refresh_per_second=8) as live:
    for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            full_reply += delta
            live.update(Panel(
                Markdown(full_reply),
                title="MagicCode",
                border_style="blue",
            ))

Rich.Live 组件在新内容流入时持续重新渲染面板。你可以看着 Markdown 表格、代码块和格式化文本在终端里实时成型——体验感拉满。

V4:工具系统——Agent 的双手

这是本文最核心的部分。前三个版本本质上是不断打磨的聊天机器人,V4 赋予 AI 真正的行动能力。

工具定义

用工厂函数简化工具定义的创建:

def _fn(name, desc, params, required):
    return {
        "type": "function",
        "function": {
            "name": name,
            "description": desc,
            "parameters": {
                "type": "object",
                "properties": params,
                "required": required
            }
        }
    }

TOOLS = [
    _fn("read_file", "读取文件内容,返回带行号的文本",
        {"path": {"type": "string", "description": "文件路径"}},
        ["path"]),
    _fn("write_file", "写入文件,自动创建父目录",
        {"path": {"type": "string"}, "content": {"type": "string"}},
        ["path", "content"]),
    _fn("edit_file", "精确替换文件中的文本(首次匹配)",
        {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}},
        ["path", "old_text", "new_text"]),
    _fn("run_command", "执行 Shell 命令,30 秒超时",
        {"command": {"type": "string"}},
        ["command"]),
    _fn("list_files", "递归列出目录结构(最多 3 层)",
        {"path": {"type": "string"}},
        []),
    _fn("search_code", "在目录中搜索代码模式",
        {"pattern": {"type": "string"}, "path": {"type": "string"}},
        ["pattern"]),
]
工具执行

AI 决定调用什么,你的代码负责真正干活。几个设计决策值得注意:

def execute_tool(name: str, params: dict) -> str:
    if name == "read_file":
        # 返回带行号的内容——方便 AI 后续精确定位
        with open(params["path"], "r", encoding="utf-8") as f:
            content = f.read()
        lines = content.split("\n")
        return "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))

    elif name == "write_file":
        # 自动创建目录——消除"目录不存在"错误
        os.makedirs(os.path.dirname(params["path"]) or ".", exist_ok=True)
        with open(params["path"], "w", encoding="utf-8") as f:
            f.write(params["content"])
        return f"Written to {params['path']}"

    elif name == "run_command":
        # 安全黑名单 + 超时保护
        dangerous = ["rm -rf /", "mkfs", "dd if=", "> /dev/sd"]
        if any(d in params["command"] for d in dangerous):
            return "Refused to execute dangerous command"
        result = subprocess.run(
            params["command"], shell=True,
            capture_output=True, text=True, timeout=30
        )
        return result.stdout or "(No output)"
    # ... 其他工具实现
Agentic Loop 实现

这是整个项目的灵魂——不到 40 行代码,实现自主决策和多步工具执行:

def chat(self, user_input: str):
    self.history.append({"role": "user", "content": user_input})
    tool_count = 0

    while True:
        # 1. 带工具定义调用 LLM
        response = client.chat.completions.create(
            model=MODEL,
            messages=self.history,
            tools=TOOLS,
        )
        message = response.choices[0].message
        self.history.append(message)

        # 2. 显示文本内容
        if message.content:
            console.print(Panel(Markdown(message.content), title="MagicCode"))

        # 3. 没有工具调用 → 任务完成,退出循环
        if not message.tool_calls:
            break

        # 4. 执行每个工具调用,将结果反馈回去
        for tc in message.tool_calls:
            tool_count += 1
            name = tc.function.name
            args = json.loads(tc.function.arguments)

            result = execute_tool(name, args)

            # 以 role="tool" 消息发送结果
            self.history.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result,
            })

        # 安全阀:防止死循环
        if tool_count > 20:
            break
        # 回到 while 顶部——AI 继续思考

精妙之处在于 while True 一个用户请求可以触发十几次工具调用,每一次都基于上一次的结果做出决策。这就是多步推理的实现方式。


完整源码架构

整合所有部分后,MagicCode 的完整架构如下:

🖥️ UI 层

Rich Panel → Markdown 渲染

交互命令 → exit / clear

🔄 Agentic Loop

chat() → 自主决策循环

安全阀 → 20 次工具调用上限

🔧 工具执行层

read_file → 带行号读取

write_file → 自动创建目录

edit_file → 精确文本替换

run_command → 安全黑名单 + 超时

list_files → 递归目录树

search_code → 模式搜索

⚙️ 配置层

MODEL 环境变量可切换

SYSTEM_PROMPT 行为规则

TOOLS 6 个工具定义

#!/usr/bin/env python3
"""
MagicCode — 从零构建的终端 AI 编程助手
演示:Agentic Loop | 工具调用 | 流式输出 | Rich UI
"""
import os
import json
import glob
import subprocess
from openai import OpenAI
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel

# ========== 配置 ==========
MODEL = os.getenv("MAGIC_MODEL", "gpt-4o")
client = OpenAI()  # 从环境变量读取 OPENAI_API_KEY

SYSTEM_PROMPT = """You are MagicCode, a powerful terminal AI coding assistant.

## Your Tools
- read_file: Read file contents (with line numbers)
- write_file: Write to files (auto-creates directories)
- edit_file: Replace specific text in a file
- run_command: Execute shell commands (30s timeout)
- list_files: List directory structure
- search_code: Search for patterns in code

## Working Principles
1. Always read a file before modifying it
2. Break complex tasks into steps; verify each step
3. Never execute destructive commands (rm -rf, format, etc.)
4. Respond in Markdown format"""

# ========== 工具定义 ==========
def _fn(name, desc, params, required):
    return {"type": "function", "function": {
        "name": name, "description": desc,
        "parameters": {"type": "object", "properties": params, "required": required},
    }}

TOOLS = [
    _fn("read_file", "Read file contents. Returns text with line numbers.",
        {"path": {"type": "string", "description": "File path"}}, ["path"]),
    _fn("write_file", "Write content to a file. Creates directories if needed.",
        {"path": {"type": "string", "description": "File path"},
         "content": {"type": "string", "description": "Complete file content"}},
        ["path", "content"]),
    _fn("edit_file", "Replace old_text with new_text in a file (first match).",
        {"path": {"type": "string", "description": "File path"},
         "old_text": {"type": "string", "description": "Text to find"},
         "new_text": {"type": "string", "description": "Replacement text"}},
        ["path", "old_text", "new_text"]),
    _fn("run_command", "Execute a shell command with 30-second timeout.",
        {"command": {"type": "string", "description": "Shell command"}},
        ["command"]),
    _fn("list_files", "Recursively list directory structure (max 3 levels).",
        {"path": {"type": "string", "description": "Directory path"}}, []),
    _fn("search_code", "Search for a pattern across all files in a directory.",
        {"pattern": {"type": "string", "description": "Search pattern"},
         "path": {"type": "string", "description": "Search directory"}},
        ["pattern"]),
]

IGNORED_DIRS = {".git", "node_modules", "__pycache__", ".venv", "venv", "dist", "build"}

# ========== 工具执行 ==========
def execute_tool(name: str, params: dict) -> str:
    try:
        if name == "read_file":
            with open(params["path"], "r", encoding="utf-8", errors="replace") as f:
                content = f.read()
            lines = content.split("\n")
            numbered = "\n".join(f"{i+1:4d} | {line}" for i, line in enumerate(lines))
            return f"{params['path']} ({len(lines)} lines)\n{numbered}"

        elif name == "write_file":
            path = params["path"]
            os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
            with open(path, "w", encoding="utf-8") as f:
                f.write(params["content"])
            return f"Written to {path} ({len(params['content'])} chars)"

        elif name == "edit_file":
            path = params["path"]
            with open(path, "r", encoding="utf-8") as f:
                content = f.read()
            if params["old_text"] not in content:
                return "Error: Target text not found in file"
            new_content = content.replace(params["old_text"], params["new_text"], 1)
            with open(path, "w", encoding="utf-8") as f:
                f.write(new_content)
            return f"Edited {path}"

        elif name == "run_command":
            cmd = params["command"]
            dangerous = ["rm -rf /", "mkfs", "dd if=", "> /dev/sd"]
            if any(d in cmd for d in dangerous):
                return "Refused to execute dangerous command"
            result = subprocess.run(
                cmd, shell=True, capture_output=True, text=True, timeout=30
            )
            output = result.stdout
            if result.stderr:
                output += "\n--- stderr ---\n" + result.stderr
            return output.strip() or "(No output)"

        elif name == "list_files":
            path = params.get("path", ".")
            lines = []
            def walk(d, prefix="", depth=0):
                if depth >= 3: return
                try: entries = sorted(os.listdir(d))
                except PermissionError: return
                for e in entries:
                    full = os.path.join(d, e)
                    if e in IGNORED_DIRS or e.startswith("."): continue
                    if os.path.isdir(full):
                        lines.append(f"{prefix}[dir] {e}/")
                        walk(full, prefix + "  ", depth + 1)
                    else:
                        lines.append(f"{prefix}[file] {e}")
            walk(path)
            return "\n".join(lines[:200]) or "Empty directory"

        elif name == "search_code":
            pattern = params["pattern"]
            path = params.get("path", ".")
            matches = []
            for fp in glob.glob(os.path.join(path, "**", "*"), recursive=True):
                if any(d in fp for d in IGNORED_DIRS) or not os.path.isfile(fp):
                    continue
                try:
                    with open(fp, "r", encoding="utf-8", errors="replace") as f:
                        for i, line in enumerate(f, 1):
                            if pattern.lower() in line.lower():
                                matches.append(f"{fp}:{i}: {line.rstrip()}")
                                if len(matches) >= 50: break
                except OSError: continue
                if len(matches) >= 50: break
            return "\n".join(matches) or f"No matches for '{pattern}'"

    except Exception as e:
        return f"Error: {type(e).__name__}: {e}"

# ========== Agentic Loop ==========
class MagicCode:
    def __init__(self):
        self.console = Console()
        self.history = [{"role": "system", "content": SYSTEM_PROMPT}]

    def chat(self, user_input: str):
        self.history.append({"role": "user", "content": user_input})
        tool_count = 0

        while True:
            response = client.chat.completions.create(
                model=MODEL, messages=self.history, tools=TOOLS,
            )
            message = response.choices[0].message
            self.history.append(message)

            # 显示文本回复
            if message.content:
                self.console.print(Panel(
                    Markdown(message.content),
                    title="MagicCode", border_style="blue", padding=(1, 2),
                ))

            # 没有工具调用意味着任务完成
            if not message.tool_calls:
                break

            # 执行每个工具调用
            for tc in message.tool_calls:
                tool_count += 1
                name = tc.function.name
                args = json.loads(tc.function.arguments)
                info = json.dumps(args, ensure_ascii=False)
                if len(info) > 120: info = info[:120] + "..."
                self.console.print(f"  [yellow][{tool_count}] {name}[/] [dim]{info}[/]")

                result = execute_tool(name, args)
                preview = result[:100].replace("\n", " ")
                self.console.print(f"  [green]  Done[/] [dim]{preview}[/]")

                self.history.append({
                    "role": "tool",
                    "tool_call_id": tc.id,
                    "content": result,
                })

            # 安全阀:防止死循环
            if tool_count > 20:
                self.console.print("[red]Tool call limit reached (20)[/]")
                break

    def run(self):
        self.console.print(Panel(
            "[bold cyan]MagicCode[/] — 你的终端 AI 编程助手\n\n"
            "  [green]工具[/]: 读写文件 | 执行命令 | 搜索代码 | 编辑文件\n"
            "  [green]命令[/]: exit 退出 | clear 清空历史",
            border_style="cyan", padding=(1, 2),
        ))
        self.console.print(f"  [dim]工作目录: {os.getcwd()}[/]")
        self.console.print(f"  [dim]模型: {MODEL}[/]\n")

        while True:
            try:
                user_input = self.console.input("[bold green]You >[/] ")
                cmd = user_input.strip().lower()
                if cmd in ("exit", "quit"): break
                elif cmd == "clear":
                    self.history = [{"role": "system", "content": SYSTEM_PROMPT}]
                    self.console.print("[dim]历史已清空[/]")
                    continue
                elif not cmd: continue
                self.chat(user_input)
                self.console.print()
            except KeyboardInterrupt:
                self.console.print("\n[cyan]再见![/]")
                break

if __name__ == "__main__":
    MagicCode().run()


6 个工具 vs 生产级 Agent

你可能会问:6 个工具够用吗?和 Claude Code 对比一下:

能力 MagicCode(6 工具) Claude Code(~15 工具)
读文件 read_file Read
写文件 write_file Write
编辑文件 edit_file Edit
执行命令 run_command Bash
列出目录 list_files Glob
搜索代码 search_code Grep
MCP 集成
多文件 Diff
Notebook 编辑
网页搜索

6 个工具覆盖了日常使用场景的约 80%。 剩下的 20% 主要是 MCP 集成、Notebook 编辑等高级功能——有用,但不是架构的核心。你掌握的核心模式完全一样。


五种扩展方向

基础已经打好,以下是让 MagicCode 更接近生产级的方向:

MagicCode
扩展方向

权限确认

只读操作直接执行

写操作需用户批准

项目上下文

读取 CLAUDE.md

读取 README.md

注入系统提示词

模型切换

DeepSeek

Ollama 本地

任意兼容接口

对话持久化

JSON 文件存储

启动时恢复历史

Token 追踪

输入 Token 统计

输出 Token 统计

费用估算

1. 权限确认

生产级 Agent 在写文件或执行命令前会征求确认:

def execute_tool_with_confirm(name, params):
    if name in ("read_file", "list_files", "search_code"):
        return execute_tool(name, params)  # 只读操作直接执行

    console.print(f"[yellow]工具请求: {name}({params})[/]")
    confirm = console.input("[bold]允许?(y/n) [/]")
    return execute_tool(name, params) if confirm.lower() == "y"
    return "User denied this operation"

2. 项目上下文加载

Claude Code 会自动读取项目根目录的 CLAUDE.md 来理解上下文。你也可以这样做:

def load_project_context():
    """加载项目配置文件作为上下文。"""
    context = ""
    for name in ["CLAUDE.md", "AGENTS.md", "README.md"]:
        if os.path.exists(name):
            with open(name, "r") as f:
                context += f"\n\n--- {name} ---\n{f.read()}"
    return context

# 追加到系统提示词
project_ctx = load_project_context()
if project_ctx:
    SYSTEM_PROMPT += f"\n\n## Project Context\n{project_ctx}"

3. 随意切换模型

MagicCode 不绑定 GPT。任何支持 Function Calling 的模型都能用。OpenAI SDK 的兼容接口让切换变得轻而易举:

# DeepSeek
client = OpenAI(api_key="your-key", base_url="https://api.deepseek.com/v1")

# 本地 Ollama
client = OpenAI(api_key="ollama", base_url="http://localhost:11434/v1")

# 其他兼容 OpenAI 接口的模型
client = OpenAI(api_key="your-key", base_url="https://your-provider.com/v1")

这也是使用 OpenAI SDK 的原因之一——它是事实上的标准接口,几乎所有模型提供商都提供兼容端点。

4. 对话持久化

目前退出程序后对话历史就丢了。用 JSON 保存下来:

import json

HISTORY_FILE = ".magiccode_history.json"

def save_history(history):
    with open(HISTORY_FILE, "w") as f:
        json.dump(history, f, ensure_ascii=False, default=str)

def load_history():
    if os.path.exists(HISTORY_FILE):
        with open(HISTORY_FILE, "r") as f:
            return json.load(f)
    return []

5. Token 用量追踪

API 调用是要花钱的。加个用量追踪很简单:

total_input_tokens = 0
total_output_tokens = 0

# 每次 API 调用后:
total_input_tokens += response.usage.prompt_tokens
total_output_tokens += response.usage.completion_tokens

# 退出时:
console.print(f"[dim]Token 统计 — 输入: {total_input_tokens} | 输出: {total_output_tokens}[/]")

常见坑和解决办法

构建第一个 Agent 时,你大概率会踩几个坑:

问题 原因 解决方案
tool_call_id 不匹配报错 工具结果没引用正确的调用 ID 始终使用响应中的 tool_call.id,不要自己生成
Agent 无限循环 没有退出条件或模型一直调用工具 tool_count 限制(推荐 20)
模型不用工具 工具描述太模糊 写具体、可操作的描述,给出使用场景
大文件导致崩溃 整个文件读进内存 加文件大小检查,截断大文件
命令卡住 subprocess.run 没设超时 始终设置 timeout=30(或合适的值)
上下文窗口溢出 对话历史太长 实现对话摘要或滑动窗口机制

与 AI Agent 生态的关联

你在这里构建的架构不只是练手——它和整个行业用的是同一套模式。根据 Anthropic 关于构建有效 Agent 的研究,带工具调用的 Agentic Loop 是所有生产级 AI Agent 的基础模式。

如果你想看这套模式如何扩展到生产级,可以把 MagicCode 和市面上的 AI 编程工具做对比。你会发现每一个的核心都是同样的三个组件:LLM API、工具定义、Agentic Loop

要进一步了解工具集成的演进方向,可以关注 MCP(Model Context Protocol)——它标准化了 AI Agent 发现和连接外部工具的方式,是 Agent 生态的下一个重要方向。


核心要点

现代 AI 编程 Agent 的整个架构归结为三件事

推理

选择

反馈

调用

🔁 神经系统

Agentic Loop
思考→行动→观察→再思考

🤲 双手

工具定义
JSON Schema 描述能力

🧠 大脑

LLM API
支持 Function Calling

  1. 一个支持 Function Calling 的 LLM API —— 大脑
  2. 描述可用能力的工具定义 —— 双手
  3. 一个编排思考与行动循环的 Agentic Loop —— 神经系统

掌握这三个概念,你就理解了 Claude Code、Cursor Agent、Copilot Workspace 以及市面上所有 AI 编程工具的核心架构。

从 20 行到 250 行,从聊天机器人到自主 Agent,你现在拥有了在这套模式上构建任何东西的基础。复制代码、运行、修改、扩展——最好的学习方式永远是亲手做一遍。


本文参考了 Bruce 的《手把手教你用 Python 实现 AI Agent》教程,在此基础上进行了重新组织和深度扩展。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐