导语:我们已经学会了使用 Function Calling,也探索了 LangChain 等成熟的 Agent 框架。但你是否曾感到,这些框架在提供便利的同时,也像一个“黑盒”,让你难以洞察其内部的运行机制?你是否渴望能完全掌控 Agent 的每一个决策、每一次心跳?本文将是你通往 Agent 架构师的“成人礼”。我们将整合前几章所学的全部知识,从最基础的组件开始,一步步“手搓”一个属于你自己的、极简但功能完备的 Agentic AI 框架。告别黑盒,洞悉原理,让你真正成为 Agent 的“造物主”。
目录
- “造轮子”的意义:为什么要亲手构建一个 Agent 框架?
- 破除“黑盒”迷信,理解第一性原理
- 获得极致的灵活性和可控性
- 为构建生产级、可定制化的 Agent 打下坚实基础
- 框架蓝图:设计我们的迷你 Agent 框架 (GAME)
- GAME 架构:
CoreAgent,Tool,Memory,Engine - 核心组件的功能与交互关系
- Mermaid 图:GAME 框架的宏观架构图
- GAME 架构:
- 组件一:
Tool—— 能力的标准化抽象- 设计
Tool基类:包含name,description,args_schema,_run - 利用 Pydantic 实现参数的自动校验与 JSON Schema 生成
- 实战:将普通 Python 函数封装成标准
Tool
- 设计
- 组件二:
Memory—— 让 Agent 拥有记忆- 设计
BaseMemory接口:add_message,get_messages - 实现一个简单的
InMemoryMemory,存储对话历史 - 思考:如何扩展实现基于 Redis 或数据库的持久化记忆?
- 设计
- 组件三:
Engine—— Agent 的“心脏”与“大脑”- 设计
Engine:封装 LLM 调用逻辑 - 实现
OpenAIEngine:集成 OpenAI/DeepSeek 的 Function Calling - 职责分离:
Engine只负责与 LLM 通信,不关心业务逻辑
- 设计
- 组件四:
CoreAgent—— 组装一切的总指挥- 设计
CoreAgent:持有Engine,Memory和Tool注册表 - 实现核心
run循环:- 从
Memory获取历史消息 - 调用
Engine获取 LLM 的决策(tool_calls或content) - 查找并执行
Tool - 更新
Memory
- 从
- 代码实战:将所有组件组装成一个可运行的 Agent
- 设计
- 运行我们的 GAME 框架!
- 创建天气工具
WeatherTool - 实例化
CoreAgent - 与我们亲手构建的 Agent 进行多轮对话
- 创建天气工具
- 总结与展望:从 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 框架将由四个核心组件构成:
- Tool (工具):能力的具体实现。这是 Agent 的“双手”。
- Memory (记忆):负责存储和管理对话历史。这是 Agent 的“海马体”。
- Engine (引擎):封装了与特定 LLM(如 OpenAI, DeepSeek)的底层交互逻辑。这是 Agent 的“语言中枢”。
- CoreAgent (核心代理):整个框架的总指挥,它持有其他所有组件,并驱动着 Agent 的核心运行循环。这是 Agent 的“决策大脑”。
核心组件的功能与交互关系
CoreAgent在接收到用户输入后,会向Memory添加这条新消息。- 然后,
CoreAgent从Memory中获取完整的对话历史,并从Tool注册表中提取所有工具的描述。 - 它将历史和工具描述一起交给
Engine。 Engine负责调用底层 LLM API,并返回 LLM 的决策(一个最终回复或一系列tool_calls)。- 如果
Engine返回了tool_calls,CoreAgent会根据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_message 和 get_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 调用。它接收 messages 和 tools 列表,并返回 LLM 响应消息的字典表示。通过这种方式,CoreAgent 无需关心具体的 API 是如何调用的,它只需要和 Engine 的 call_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),
}
)
CoreAgent 的 run 方法完美地实现了我们在蓝图中设计的核心循环。它的逻辑非常清晰,每一步都一目了然,没有任何“黑盒”。
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 的每一步思考和行动:
- 用户输入 "What's the weather in Tokyo?"。
CoreAgent调用OpenAIEngine。OpenAIEngine向 DeepSeek API 发送了包含get_current_weather和get_stock_price两个工具描述的请求。- LLM 返回了调用
get_current_weather的tool_calls。 CoreAgent查找到weather_tool并执行了它的_run方法。CoreAgent将天气结果存入InMemoryMemory,并开始下一次循环。- 在第二次循环中,LLM 看到了天气结果,生成了最终的自然语言回复。
- Agent 等待你的下一次输入,整个过程无缝衔接。
8. 总结与展望:从 GAME 框架到生产级系统
恭喜你!你已经成功地构建并运行了一个完全属于自己的 Agentic AI 框架。通过这个过程,Agent 框架在你面前已经不再是神秘的“黑盒”。
我们的迷你框架与 LangChain 的异同
- 相似之处:核心思想高度一致,都是围绕“LLM + Tools + Prompt/Memory”的模式构建。我们的
CoreAgentrun循环,与 LangChainAgentExecutor的内部逻辑本质上是相同的。 - 不同之处:
- 抽象层次:GAME 框架的抽象层次更低,代码更“扁平”,让你能直接接触到核心循环。LangChain 拥有更高、更复杂的抽象(如
LCEL),以链式调用的方式隐藏了循环。 - 功能完备性:GAME 只实现了最核心的功能。LangChain 提供了海量的内置工具、记忆类型、Agent 变体、回调系统等,是一个工业级的庞大生态。
- 抽象层次:GAME 框架的抽象层次更低,代码更“扁平”,让你能直接接触到核心循环。LangChain 拥有更高、更复杂的抽象(如
下一步:如何为 GAME 框架增加新功能?
拥有了自己的框架后,你可以轻易地为其“添砖加瓦”:
- 流式输出 (Streaming):修改
OpenAIEngine,在调用 LLM API 时加入stream=True,并逐块yield返回内容。同时修改CoreAgent以处理流式响应。 - 异步处理 (Async):使用
asyncio和httpx,将Engine和Tool的_run方法改造为async函数,使CoreAgent能够并发执行多个工具调用。 - 回调系统 (Callbacks):在
CoreAgent的关键节点(如on_llm_start,on_tool_end)设置回调函数接口,允许外部代码挂载日志、监控等功能。
你的 Agent,你做主!
亲手构建 GAME 框架的经历,为你提供了深入理解、评估和选择任何 Agent 框架所需的“X光视野”。更重要的是,它赋予了你“如果现有框架都不合适,我就自己造一个”的底气和能力。这,正是一个高级 AI 工程师和系统架构师的核心价值所在。