1.6 告别黑盒!从零到一,手搓一个属于你自己的 Agentic AI 框架

2 阅读1分钟

导语:我们已经学会了使用 Function Calling,也探索了 LangChain 等成熟的 Agent 框架。但你是否曾感到,这些框架在提供便利的同时,也像一个“黑盒”,让你难以洞察其内部的运行机制?你是否渴望能完全掌控 Agent 的每一个决策、每一次心跳?本文将是你通往 Agent 架构师的“成人礼”。我们将整合前几章所学的全部知识,从最基础的组件开始,一步步“手搓”一个属于你自己的、极简但功能完备的 Agentic AI 框架。告别黑盒,洞悉原理,让你真正成为 Agent 的“造物主”。

目录

  1. “造轮子”的意义:为什么要亲手构建一个 Agent 框架?
    • 破除“黑盒”迷信,理解第一性原理
    • 获得极致的灵活性和可控性
    • 为构建生产级、可定制化的 Agent 打下坚实基础
  2. 框架蓝图:设计我们的迷你 Agent 框架 (GAME)
    • GAME 架构:CoreAgent, Tool, Memory, Engine
    • 核心组件的功能与交互关系
    • Mermaid 图:GAME 框架的宏观架构图
  3. 组件一:Tool —— 能力的标准化抽象
    • 设计 Tool 基类:包含 name, description, args_schema, _run
    • 利用 Pydantic 实现参数的自动校验与 JSON Schema 生成
    • 实战:将普通 Python 函数封装成标准 Tool
  4. 组件二:Memory —— 让 Agent 拥有记忆
    • 设计 BaseMemory 接口:add_message, get_messages
    • 实现一个简单的 InMemoryMemory,存储对话历史
    • 思考:如何扩展实现基于 Redis 或数据库的持久化记忆?
  5. 组件三:Engine —— Agent 的“心脏”与“大脑”
    • 设计 Engine:封装 LLM 调用逻辑
    • 实现 OpenAIEngine:集成 OpenAI/DeepSeek 的 Function Calling
    • 职责分离:Engine 只负责与 LLM 通信,不关心业务逻辑
  6. 组件四:CoreAgent —— 组装一切的总指挥
    • 设计 CoreAgent:持有 Engine, MemoryTool 注册表
    • 实现核心 run 循环:
      • Memory 获取历史消息
      • 调用 Engine 获取 LLM 的决策(tool_callscontent
      • 查找并执行 Tool
      • 更新 Memory
    • 代码实战:将所有组件组装成一个可运行的 Agent
  7. 运行我们的 GAME 框架!
    • 创建天气工具 WeatherTool
    • 实例化 CoreAgent
    • 与我们亲手构建的 Agent 进行多轮对话
  8. 总结与展望:从 GAME 框架到生产级系统
    • 我们的迷你框架与 LangChain 的异同
    • 下一步:如何为 GAME 框架增加流式输出、异步处理、回调系统?
    • 你的 Agent,你做主!

1. “造轮子”的意义:为什么要亲手构建一个 Agent 框架?

在软件工程领域,“不要重复造轮子”是一条广为人知的建议。然而,在学习和掌握一门新技术时,亲手“造一次轮子”却往往是通往精通的捷径。对于 Agentic AI 框架,更是如此。

破除“黑盒”迷信,理解第一性原理

LangChain 这样的框架功能强大,但其高度的抽象和复杂的调用链也使其成为一个巨大的“黑盒”。当出现问题时,我们往往只能猜测其内部发生了什么。

通过亲手构建一个框架,哪怕它非常简单,我们也能被迫去思考和实现最核心的逻辑:

  • 工具是如何被描述和调用的?
  • 对话历史是如何被管理和传递的?
  • Agent 的决策循环究竟是怎样运转的?

这个过程能帮助我们建立起对 Agent 工作原理的**第一性原理(First Principles)**认知。掌握了这些根本原理,你再去看 LangChain 或其他任何框架,就能迅速洞穿其表象,直达其设计核心。

获得极致的灵活性和可控性

预制的框架就像标准化的成衣,虽然方便,但总有不合身的地方。在真实的业务场景中,我们常常需要对 Agent 的行为进行精细的定制:

  • 实现特定的日志记录格式。
  • 介入 LLM 的 Prompt 构建过程。
  • 设计独特的错误处理和重试逻辑。
  • 集成公司内部的认证授权系统。

在庞大的框架中进行“魔改”往往是痛苦且难以维护的。而拥有一个自己构建的、简洁的框架,你就可以像上帝一样,随心所欲地修改和扩展它的任何部分,以 100% 满足你的业务需求。

2. 框架蓝图:设计我们的迷你 Agent 框架 (GAME)

在开始写代码前,我们需要一份清晰的蓝图。我们将我们的迷你框架命名为 GAME (Generic Agentic Micro-framework Engine)。

GAME 框架将由四个核心组件构成:

  1. Tool (工具):能力的具体实现。这是 Agent 的“双手”。
  2. Memory (记忆):负责存储和管理对话历史。这是 Agent 的“海马体”。
  3. Engine (引擎):封装了与特定 LLM(如 OpenAI, DeepSeek)的底层交互逻辑。这是 Agent 的“语言中枢”。
  4. CoreAgent (核心代理):整个框架的总指挥,它持有其他所有组件,并驱动着 Agent 的核心运行循环。这是 Agent 的“决策大脑”。

核心组件的功能与交互关系

  • CoreAgent 在接收到用户输入后,会向 Memory 添加这条新消息。
  • 然后,CoreAgentMemory 中获取完整的对话历史,并从 Tool 注册表中提取所有工具的描述。
  • 它将历史和工具描述一起交给 Engine
  • Engine 负责调用底层 LLM API,并返回 LLM 的决策(一个最终回复或一系列 tool_calls)。
  • 如果 Engine 返回了 tool_callsCoreAgent 会根据 tool_calls 中的工具名,在 Tool 注册表中找到对应的 Tool 实例并执行它。
  • CoreAgent 将工具的执行结果再次存入 Memory,然后开始一个新的循环,直到 Engine 返回一个最终回复。

Mermaid 图:GAME 框架的宏观架构图

graph TD
    subgraph CoreAgent
        A[run() 循环]
    end

    subgraph Engine
        B[call_llm()]
    end

    subgraph Memory
        C[add_message()]
        D[get_messages()]
    end

    subgraph Tools
        E[Tool Registry]
        F[WeatherTool]
        G[CalculatorTool]
    end

    A -- "1. 获取历史, 准备 Prompt" --> D;
    D -- "2. 返回 Messages" --> A;
    A -- "3. 调用 LLM 决策" --> B;
    B -- "4. 返回 LLM 响应 (content/tool_calls)" --> A;
    A -- "5. 查找并执行 Tool" --> E;
    E --> F;
    F -- "6. 返回 Tool 结果" --> A;
    A -- "7. 记录 Tool 结果" --> C;
    C --> A;

3. 组件一:Tool —— 能力的标准化抽象

Tool 是框架中最基础的组件。我们需要设计一个标准化的接口,让任何 Python 函数都能被方便地封装成一个 Tool。我们将使用 Pydantic 这个强大的库来帮助我们处理参数校验和 JSON Schema 生成。

环境准备

pip install pydantic openai

代码实现

创建一个新文件 game_framework/components.py

# game_framework/components.py
from abc import ABC, abstractmethod
from typing import List, Dict, Any, Type
import json

from pydantic import BaseModel, create_model

# --- Tool Component ---

class BaseTool(ABC):
    """工具的基类"""
    name: str = "base_tool"
    description: str = "A base tool"
    args_schema: Type[BaseModel] = BaseModel # 默认没有参数
    
    @abstractmethod
    def _run(self, *args, **kwargs) -> Any:
        """工具的具体执行逻辑"""
        pass

    def get_openai_tool_schema(self) -> Dict[str, Any]:
        """生成符合 OpenAI Function Calling 格式的 JSON Schema"""
        # 利用 Pydantic 的 model_json_schema() 方法
        schema = self.args_schema.model_json_schema()
        
        # Pydantic v2 移除了 'title' 和 'description'
        # 我们需要从 schema 中移除它们,因为 OpenAI API 不接受
        schema.pop("title", None)
        schema.pop("description", None)

        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": schema,
            },
        }

def tool(name: str, description: str, args_schema: Type[BaseModel]):
    """
    一个装饰器,用于将普通函数快速转换为一个继承自 BaseTool 的类
    """
    def decorator(func):
        # 动态创建一个继承自 BaseTool 的类
        class DynamicTool(BaseTool):
            def __init__(self):
                self.name = name
                self.description = description
                self.args_schema = args_schema
            
            def _run(self, **kwargs) -> Any:
                # 校验参数
                validated_args = self.args_schema(**kwargs)
                return func(**validated_args.model_dump())

        # setattr(DynamicTool, '__name__', func.__name__.capitalize() + "Tool")
        return DynamicTool()
    
    return decorator

# --- 实战:将普通 Python 函数封装成标准 Tool ---

# 1. 为工具的参数定义 Pydantic 模型
class WeatherArgs(BaseModel):
    location: str
    unit: str = "celsius"

# 2. 使用 @tool 装饰器
@tool(
    name="get_current_weather",
    description="Get the current weather in a given location.",
    args_schema=WeatherArgs
)
def get_weather(location: str, unit: str) -> dict:
    """这是一个普通的天气查询函数"""
    print(f"Executing get_weather with location={location}, unit={unit}")
    # 模拟 API 调用
    if "tokyo" in location.lower():
        return {"location": "Tokyo", "temperature": 10, "unit": "celsius"}
    # ... (其他逻辑)
    return {"location": location, "temperature": "unknown"}

# get_weather 现在是一个 BaseTool 的实例
print("--- Tool Created ---")
print(f"Tool Name: {get_weather.name}")
print("--- Tool OpenAI Schema ---")
print(json.dumps(get_weather.get_openai_tool_schema(), indent=2))

在这段代码中:

  • 我们定义了 BaseTool 抽象类,规定了所有工具都必须有 name, description, args_schema 和一个 _run 方法。
  • get_openai_tool_schema 方法巧妙地利用了 Pydantic 模型的 model_json_schema() 方法,自动地、无错误地生成了符合 OpenAI 要求的 JSON Schema。
  • 我们创建了一个 @tool 装饰器,这是一个“语法糖”,它可以将任何普通函数(如 get_weather)连同其参数模型(WeatherArgs)方便地转换成一个 BaseTool 的实例。这大大简化了工具的创建过程。

4. 组件二:Memory —— 让 Agent 拥有记忆

Memory 组件相对简单,它的核心职责就是记录对话的每一条消息。

代码实现 (继续在 game_framework/components.py 中添加)

# game_framework/components.py (续)

# --- Memory Component ---

class BaseMemory(ABC):
    @abstractmethod
    def add_message(self, message: Dict[str, Any]):
        pass

    @abstractmethod
    def get_messages(self) -> List[Dict[str, Any]]:
        pass

class InMemoryMemory(BaseMemory):
    """一个简单的、存在于内存中的记忆实现"""
    def __init__(self):
        self._messages: List[Dict[str, Any]] = []

    def add_message(self, message: Dict[str, Any]):
        self._messages.append(message)

    def get_messages(self) -> List[Dict[str, Any]]:
        return self._messages

我们定义了 BaseMemory 接口和它的一个简单实现 InMemoryMemory。这种基于接口的设计使得未来扩展变得非常容易。例如,要实现一个基于 Redis 的记忆,我们只需要创建一个 RedisMemory 类,实现同样的 add_messageget_messages 方法即可,而 Agent 的其他部分代码无需任何改动。

5. 组件三:Engine —— Agent 的“心脏”与“大脑”

Engine 的职责是单一且明确的:封装与底层 LLM 的所有交互细节,向上层(CoreAgent)提供一个干净的接口。

代码实现 (继续在 game_framework/components.py 中添加)

# game_framework/components.py (续)
from openai import OpenAI

# --- Engine Component ---

class BaseEngine(ABC):
    @abstractmethod
    def call_llm(self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]]) -> Dict[str, Any]:
        pass

class OpenAIEngine(BaseEngine):
    """封装 OpenAI/DeepSeek 等兼容 API 的引擎"""
    def __init__(self, model: str = "deepseek-chat"):
        self.client = OpenAI() # 确保已配置 API Key 等环境变量
        self.model = model

    def call_llm(self, messages: List[Dict[str, Any]], tools: List[Dict[str, Any]]) -> Dict[str, Any]:
        print("--- Calling LLM Engine ---")
        print(f"Messages: {messages}")
        print(f"Tools: {tools}")
        
        response = self.client.chat.completions.create(
            model=self.model,
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )
        return response.choices[0].message.model_dump()

OpenAIEngine 封装了 openai.chat.completions.create 调用。它接收 messagestools 列表,并返回 LLM 响应消息的字典表示。通过这种方式,CoreAgent 无需关心具体的 API 是如何调用的,它只需要和 Enginecall_llm 方法交互即可。

6. 组件四:CoreAgent —— 组装一切的总指挥

现在,万事俱备,只欠东风。CoreAgent 将把我们之前创建的所有组件像乐高积木一样组装起来,驱动整个 Agent 运转。

代码实现

创建一个新文件 game_framework/agent.py

# game_framework/agent.py

from typing import List, Dict, Any
from .components import BaseTool, BaseMemory, BaseEngine

class CoreAgent:
    def __init__(self, engine: BaseEngine, memory: BaseMemory, tools: List[BaseTool]):
        self.engine = engine
        self.memory = memory
        self.tool_registry: Dict[str, BaseTool] = {tool.name: tool for tool in tools}

    def _get_tool_schemas(self) -> List[Dict[str, Any]]:
        return [tool.get_openai_tool_schema() for tool in self.tool_registry.values()]

    def run(self, user_prompt: str):
        print(f"\n>>>>>>>>>>>> User Prompt: {user_prompt} <<<<<<<<<<<<\n")
        self.memory.add_message({"role": "user", "content": user_prompt})

        while True:
            # 1. 从 Memory 获取历史消息
            messages = self.memory.get_messages()
            # 2. 从 Tool Registry 获取工具描述
            tool_schemas = self._get_tool_schemas()

            # 3. 调用 Engine 获取 LLM 决策
            llm_response = self.engine.call_llm(messages, tool_schemas)
            self.memory.add_message(llm_response)

            # 4. 解析 LLM 响应,判断是否需要调用工具
            tool_calls = llm_response.get("tool_calls")
            if not tool_calls:
                # 如果没有工具调用,说明是最终回复
                final_answer = llm_response.get("content")
                print(f"\n----------- Final Answer -----------\n{final_answer}\n----------------------------------\n")
                return final_answer
            
            # 5. 执行工具调用
            print("--- Executing Tools ---")
            for tool_call in tool_calls:
                tool_name = tool_call["function"]["name"]
                tool_args = json.loads(tool_call["function"]["arguments"])
                
                if tool_name not in self.tool_registry:
                    print(f"Error: Tool '{tool_name}' not found.")
                    # 可以选择向 LLM 报告错误
                    tool_result = {"error": f"Tool '{tool_name}' not found."}
                else:
                    # 查找并执行 Tool
                    target_tool = self.tool_registry[tool_name]
                    try:
                        tool_result = target_tool._run(**tool_args)
                    except Exception as e:
                        print(f"Error executing tool {tool_name}: {e}")
                        tool_result = {"error": str(e)}

                # 6. 将工具执行结果存入 Memory,准备下一次循环
                self.memory.add_message(
                    {
                        "tool_call_id": tool_call["id"],
                        "role": "tool",
                        "name": tool_name,
                        "content": json.dumps(tool_result),
                    }
                )

CoreAgentrun 方法完美地实现了我们在蓝图中设计的核心循环。它的逻辑非常清晰,每一步都一目了然,没有任何“黑盒”。

7. 运行我们的 GAME 框架!

是时候见证奇迹了!让我们创建一个 main.py 文件来运行我们亲手打造的框架。

# main.py

# 从我们的框架中导入所有组件
from game_framework.agent import CoreAgent
from game_framework.components import OpenAIEngine, InMemoryMemory, tool, WeatherArgs

# 1. 创建你的工具
# 我们也可以直接导入在 components.py 中已经创建好的 get_weather 工具
from game_framework.components import get_weather as weather_tool

@tool(
    name="get_stock_price",
    description="Get the latest stock price for a given ticker symbol.",
    args_schema=create_model("StockArgs", ticker=(str, ...))
)
def get_stock_price(ticker: str) -> dict:
    if ticker.upper() == "AAPL":
        return {"ticker": "AAPL", "price": 150.0}
    return {"ticker": ticker, "price": "unknown"}


# 2. 实例化所有组件
my_engine = OpenAIEngine(model="deepseek-chat") # or "gpt-4-turbo"
my_memory = InMemoryMemory()
my_tools = [weather_tool, get_stock_price]

# 3. 组装 Agent
my_agent = CoreAgent(
    engine=my_engine,
    memory=my_memory,
    tools=my_tools,
)

# 4. 运行 Agent 并进行多轮对话
if __name__ == "__main__":
    # 第一次对话
    my_agent.run("What's the weather in Tokyo?")
    
    # 第二次对话(Agent 会记住上文)
    my_agent.run("Great. Now, what's the stock price for AAPL?")

在你的终端运行 python main.py。你会看到详细的日志,清晰地展示了 Agent 的每一步思考和行动:

  1. 用户输入 "What's the weather in Tokyo?"。
  2. CoreAgent 调用 OpenAIEngine
  3. OpenAIEngine 向 DeepSeek API 发送了包含 get_current_weatherget_stock_price 两个工具描述的请求。
  4. LLM 返回了调用 get_current_weathertool_calls
  5. CoreAgent 查找到 weather_tool 并执行了它的 _run 方法。
  6. CoreAgent 将天气结果存入 InMemoryMemory,并开始下一次循环。
  7. 在第二次循环中,LLM 看到了天气结果,生成了最终的自然语言回复。
  8. Agent 等待你的下一次输入,整个过程无缝衔接。

8. 总结与展望:从 GAME 框架到生产级系统

恭喜你!你已经成功地构建并运行了一个完全属于自己的 Agentic AI 框架。通过这个过程,Agent 框架在你面前已经不再是神秘的“黑盒”。

我们的迷你框架与 LangChain 的异同

  • 相似之处:核心思想高度一致,都是围绕“LLM + Tools + Prompt/Memory”的模式构建。我们的 CoreAgent run 循环,与 LangChain AgentExecutor 的内部逻辑本质上是相同的。
  • 不同之处
    • 抽象层次:GAME 框架的抽象层次更低,代码更“扁平”,让你能直接接触到核心循环。LangChain 拥有更高、更复杂的抽象(如 LCEL),以链式调用的方式隐藏了循环。
    • 功能完备性:GAME 只实现了最核心的功能。LangChain 提供了海量的内置工具、记忆类型、Agent 变体、回调系统等,是一个工业级的庞大生态。

下一步:如何为 GAME 框架增加新功能?

拥有了自己的框架后,你可以轻易地为其“添砖加瓦”:

  • 流式输出 (Streaming):修改 OpenAIEngine,在调用 LLM API 时加入 stream=True,并逐块 yield 返回内容。同时修改 CoreAgent 以处理流式响应。
  • 异步处理 (Async):使用 asynciohttpx,将 EngineTool_run 方法改造为 async 函数,使 CoreAgent 能够并发执行多个工具调用。
  • 回调系统 (Callbacks):在 CoreAgent 的关键节点(如 on_llm_start, on_tool_end)设置回调函数接口,允许外部代码挂载日志、监控等功能。

你的 Agent,你做主!

亲手构建 GAME 框架的经历,为你提供了深入理解、评估和选择任何 Agent 框架所需的“X光视野”。更重要的是,它赋予了你“如果现有框架都不合适,我就自己造一个”的底气和能力。这,正是一个高级 AI 工程师和系统架构师的核心价值所在。