课程0基础Agent开发课 / Agent基础 / 从零手写一个Agent框架-理解Agent的本质
— 15 min read

从零手写一个Agent框架-理解Agent的本质

本章不使用任何框架,从零编写一个能实际运行的 Agent——大约 120 行 Python,包含工具注册、LLM 调用、循环逻辑、错误处理。通过手写实现,可以清晰理解 LangChain(一个构建 LLM 应用的开源 Python 框架,封装了 Agent 循环、工具调用、链式执行等复杂逻辑)那个循环到底在干什么,以及框架封装了哪些工程问题。

从零手写一个 Agent 框架:理解 Agent 的本质

本章不使用任何框架,从零编写一个能实际运行的 Agent——大约 120 行 Python,包含工具注册、LLM 调用、循环逻辑、错误处理。通过手写实现,可以清晰理解 LangChain(一个构建 LLM 应用的开源 Python 框架,封装了 Agent 循环、工具调用、链式执行等复杂逻辑)那个循环到底在干什么,以及框架封装了哪些工程问题。

1.1 Agent 的本质是什么

工具调用请求

执行工具
返回结果

最终回答

用户输入

记忆模块
messages 列表

LLM 核心
决策与推理

工具注册表
可用工具定义

输出结果

极简 Agent 框架的四大组成——LLM 核心、工具注册表、执行循环、记忆模块

先把本质说清楚,再写代码。

一个 LLM-based Agent,本质上是三件事的组合:

一个循环:持续运行,直到任务完成或达到上限。每次迭代,模型要么输出最终答案(退出循环),要么输出工具调用请求(继续循环)。

工具调用:模型通过 function calling 告诉外部系统"我要调用某个工具,参数是这些"。外部系统执行工具,把结果返回给模型。

状态管理:所有的上下文——用户输入、模型输出、工具调用请求、工具执行结果——都存在一个叫 messages 的列表里。每次循环都把新信息追加进去,模型每次都能看到完整历史。

就这三件事。LangChain 的 AgentExecutor,LangGraph(LangChain 旗下专门用于构建有状态 Agent 工作流的框架)的 StateGraph,CrewAI(一个专注于多 Agent 协作的 Python 框架,让多个 Agent 像团队成员一样分工合作)的 Crew,底层都在做这三件事,只是在上面加了更多抽象层。

1.2 手写 Agent:完整代码

python
# agent_from_scratch.py
# 依赖:pip install openai
# 需要设置环境变量:OPENAI_API_KEY

import json
import inspect
import os
from typing import Any, Callable
from openai import OpenAI

client = OpenAI()

# ─────────────────────────────────────────
# 第一部分:工具注册系统
# ─────────────────────────────────────────

# 全局工具注册表
_tool_registry: dict[str, dict] = {}


def tool(func: Callable) -> Callable:
    """
    工具注册装饰器。
    读取函数签名和 docstring,自动生成 OpenAI function calling 所需的 schema。
    """
    sig = inspect.signature(func)
    properties = {}
    required = []

    for param_name, param in sig.parameters.items():
        # 从类型注解提取类型
        annotation = param.annotation
        if annotation == inspect.Parameter.empty:
            param_type = "string"
        elif annotation == int:
            param_type = "integer"
        elif annotation == float:
            param_type = "number"
        elif annotation == bool:
            param_type = "boolean"
        else:
            param_type = "string"

        properties[param_name] = {"type": param_type}

        # 没有默认值的参数是必填项
        if param.default == inspect.Parameter.empty:
            required.append(param_name)

    # 把函数信息存入注册表
    _tool_registry[func.__name__] = {
        "function": func,
        "schema": {
            "type": "function",
            "function": {
                "name": func.__name__,
                "description": func.__doc__ or "",
                "parameters": {
                    "type": "object",
                    "properties": properties,
                    "required": required,
                }
            }
        }
    }

    return func


def get_tools_schema() -> list[dict]:
    """返回所有注册工具的 schema,用于传给 OpenAI API"""
    return [info["schema"] for info in _tool_registry.values()]


def execute_tool(name: str, arguments: dict) -> Any:
    """根据工具名和参数执行工具,返回结果"""
    if name not in _tool_registry:
        raise ValueError(f"工具 '{name}' 未注册")
    func = _tool_registry[name]["function"]
    return func(**arguments)


# ─────────────────────────────────────────
# 第二部分:工具定义
# ─────────────────────────────────────────

@tool
def get_weather(city: str) -> str:
    """查询指定城市的当前天气和温度。"""
    # 实际项目中这里调用真实天气 API
    weather_data = {
        "北京": "晴,22°C,东南风 3 级,空气质量良",
        "上海": "多云,25°C,东风 2 级,空气质量优",
        "广州": "小雨,28°C,南风 4 级,空气质量中",
        "深圳": "晴,27°C,西南风 3 级,空气质量良",
    }
    return weather_data.get(city, f"抱歉,暂无 {city} 的天气数据")


@tool
def calculate(expression: str) -> str:
    """
    计算数学表达式。支持加减乘除、幂运算。
    示例:'2 + 3 * 4'、'(10 + 5) / 3'、'2 ** 10'
    """
    try:
        # 只允许安全的数学运算
        allowed = set("0123456789+-*/().**% ")
        if not all(c in allowed for c in expression):
            return "错误:不支持的字符,只允许数字和基本运算符"
        result = eval(expression, {"__builtins__": {}})
        return f"{expression} = {result}"
    except ZeroDivisionError:
        return "错误:除数不能为零"
    except Exception as e:
        return f"计算出错:{str(e)}"


@tool
def search_knowledge(query: str) -> str:
    """搜索知识库,查找相关信息。"""
    # 模拟知识库查询
    knowledge = {
        "python": "Python 是一种高级编程语言,以简洁易读著称,广泛用于 AI/ML、Web 开发、自动化脚本等领域。",
        "agent": "AI Agent 是一种能感知环境、做决策、执行行动的自主系统,通过 LLM 提供推理能力。",
        "langchain": "LangChain 是一个构建 LLM 应用的开源框架,提供工具链、记忆、Agent 等抽象。",
    }
    query_lower = query.lower()
    for key, value in knowledge.items():
        if key in query_lower:
            return value
    return f"知识库中未找到关于 '{query}' 的相关信息"


# ─────────────────────────────────────────
# 第三部分:Agent 核心循环
# ─────────────────────────────────────────

class AgentFromScratch:
    def __init__(
        self,
        model: str = "gpt-4o",
        max_iterations: int = 10,
        system_prompt: str = None,
    ):
        self.model = model
        self.max_iterations = max_iterations
        self.system_prompt = system_prompt or (
            "你是一个有帮助的 AI 助手。当需要时,使用提供的工具来完成任务。"
            "工具调用完成后,根据工具返回的结果给出最终答案。"
        )

    def run(self, user_input: str, verbose: bool = True) -> str:
        """
        运行 Agent,返回最终答案。
        verbose=True 时打印每步执行过程。
        """
        # 初始化 messages:系统 prompt + 用户输入
        messages = [
            {"role": "system", "content": self.system_prompt},
            {"role": "user", "content": user_input},
        ]

        if verbose:
            print(f"\n用户输入:{user_input}")
            print("─" * 50)

        # Agent 循环
        for iteration in range(self.max_iterations):
            if verbose:
                print(f"\n[迭代 {iteration + 1}] 调用 LLM...")

            # 调用 LLM
            response = client.chat.completions.create(
                model=self.model,
                messages=messages,
                tools=get_tools_schema(),
                tool_choice="auto",  # 让模型自己决定要不要调工具
            )

            message = response.choices[0].message

            # 情况一:模型决定直接回答,不调工具——退出循环
            if not message.tool_calls:
                if verbose:
                    print(f"\n最终回答:{message.content}")
                return message.content

            # 情况二:模型决定调用工具
            # 先把模型的回应(含工具调用请求)加入 messages
            messages.append(message)

            if verbose:
                print(f"模型请求调用 {len(message.tool_calls)} 个工具")

            # 执行每一个工具调用
            for tool_call in message.tool_calls:
                tool_name = tool_call.function.name
                tool_args = json.loads(tool_call.function.arguments)

                if verbose:
                    print(f"  -> 调用工具:{tool_name}({tool_args})")

                # 执行工具,处理错误
                try:
                    tool_result = execute_tool(tool_name, tool_args)
                    result_str = str(tool_result)
                except Exception as e:
                    result_str = f"工具执行失败:{str(e)}"

                if verbose:
                    print(f"     结果:{result_str}")

                # 把工具执行结果加入 messages
                # 注意:tool_call_id 必须和请求里的 id 对应
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": result_str,
                })

            # 工具执行完毕,继续循环,让模型基于结果决定下一步

        # 达到最大迭代次数,强制返回
        if verbose:
            print(f"\n达到最大迭代次数 ({self.max_iterations}),强制返回")
        return "抱歉,任务处理超出最大步骤限制,未能完成。"


# ─────────────────────────────────────────
# 第四部分:运行示例
# ─────────────────────────────────────────

if __name__ == "__main__":
    agent = AgentFromScratch(model="gpt-4o", max_iterations=10)

    # 测试 1:单工具调用
    agent.run("北京今天天气怎么样?需要带雨伞吗?")

    print("\n" + "=" * 60 + "\n")

    # 测试 2:数学计算
    agent.run("帮我计算 (2 ** 8 + 100) / 3 的结果,保留两位小数")

    print("\n" + "=" * 60 + "\n")

    # 测试 3:多工具调用
    agent.run("北京和上海今天的天气分别是什么?哪个城市更适合户外运动?")

    print("\n" + "=" * 60 + "\n")

    # 测试 4:不需要调工具的问题
    agent.run("你好,能简单介绍一下你自己吗?")

1.3 执行流程

用户输入

初始化 messages
system + user

调用 LLM

模型有工具调用?

返回最终答案
退出循环

把模型回应加入 messages

执行工具

工具执行成功?

把工具结果加入 messages

把错误信息加入 messages

达到最大迭代次数?

返回超限提示
强制退出

1.4 理解 messages 的结构

messages 列表是整个 Agent 的"工作记忆",理解它的结构就理解了一半 Agent 的工作原理。

一次完整的工具调用交互,messages 里会出现四种角色:

python
messages = [
    # 系统指令
    {"role": "system", "content": "你是一个助手..."},

    # 用户问题
    {"role": "user", "content": "北京今天天气怎样?"},

    # 模型决定调工具(注意:tool_calls 字段,content 可以为 None)
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [{
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "get_weather",
                "arguments": '{"city": "北京"}'
            }
        }]
    },

    # 工具执行结果(tool_call_id 必须和上面的 id 对应)
    {
        "role": "tool",
        "tool_call_id": "call_abc123",
        "content": "晴,22°C,东南风 3 级"
    },

    # 模型基于工具结果给出最终回答
    {
        "role": "assistant",
        "content": "北京今天天气晴朗,气温 22°C,东南风,不需要带雨伞。"
    }
]

每次循环,都在往这个列表里追加新内容。模型每次调用都能看到完整历史,这就是 Agent 能"记住"之前做了什么的原因。

1.5 手写版的局限,以及为什么需要框架

这段代码能跑,但和生产级的框架比,缺了很多东西。

没有流式输出。现在用户要等 Agent 全部执行完才能看到结果。LangChain/LangGraph 支持流式推送,每生成一个 token 就推给前端,用户体验好得多。

没有可观测性。这段代码用 print 打印执行过程,生产环境里需要结构化的日志、追踪每一次 LLM 调用的延迟和 token 消耗、在 Langfuse 或 LangSmith 上可视化整个执行链路。

没有持久化。每次调用 agent.run() 都是全新的,没有多轮对话记忆,没有把执行状态保存到数据库。LangGraph 的 Checkpoint 机制就是解决这个问题的。

没有并发控制。多工具调用时,串行执行每个工具。如果同时查北京和上海的天气,完全可以并行调用,节省时间。

没有 Human-in-the-loop。某些关键操作(比如发送邮件、提交订单)执行前应该等人工确认。LangGraph 的 interrupt 机制就是做这个的。

这就是为什么框架存在——不是为了藏起复杂性,而是把这些通用的工程问题统一解决好,让开发者专注在业务逻辑上。

但如果不理解底层的循环是怎么工作的,就没法调试框架出的问题。Agent 在生产环境里出现奇怪行为时——比如工具调用一直循环不退出、或者模型明明有工具却不调——不知道该去哪里找原因。

知道 AgentExecutor 底层是在维护一个 messages 列表、每次循环追加内容,就知道去检查 messages 的内容。知道模型的退出条件是"不再输出 tool_calls",就知道去检查 system prompt 里有没有说清楚什么时候应该直接回答。

框架用起来,原理懂了,出了问题才知道去哪里找。

本页目录