拆解 Agent 框架:四层抽象,三十行代码的本质

上篇文章 AI Agent 从零到一 里,我用 30 行代码手写了一个 tool calling 循环。很多读者看完后的反应是:"原来 Agent 框架底层就这么简单?"

是的。但框架毕竟不只是那个循环。它还做了更多工程化的事情。

这篇文章往上走一层——把一个 Agent 框架拆成四层,每一层用手写代码和 qwen-agent 框架做对照。读完你会理解:你在框架文档里看到的每个概念,在底层对应着什么。


📦 相关链接


🎯 问题:框架到底做了什么?

上篇文章的核心代码是一个 while True 循环:

while True:
    response = client.chat.completions.create(model=MODEL, messages=messages, tools=TOOLS_SCHEMA)
    msg = response.choices[0].message
    if not msg.tool_calls:
        return msg.content
    for tc in msg.tool_calls:
        result = TOOL_MAP[tc.function.name](**args)
        messages.append({"role": "tool", "content": result})

这段代码能跑,但它把所有东西揉在了一起:连接模型、定义工具、管理消息、执行循环。

任何一个真实的 Agent 框架都会把这坨代码拆开,形成清晰的层次结构。不同的框架用不同的名字和 API,但拆分逻辑是一样的。


🧱 四层抽象:Client → Agent → Tools → Session

我读完了 Microsoft Agent Framework、LangChain 和 qwen-agent 的源码后,总结出所有 Agent 框架共有的四层抽象:

┌──────────────────────────────────────────────┐
│                                              │
│   Client ──→ Agent ──→ Tools                 │
│   连接         封装       能力                 │
│                │                              │
│                └──→ Session                   │
│                     记忆                      │
│                                              │
└──────────────────────────────────────────────┘

每一层解决一个独立的问题。下面逐层拆解。


第一层:Client — 连接模型

职责:和 AI 模型建立连接,处理认证、请求格式化、响应解析。

在原生 SDK 里,Client 就是 OpenAI(...) 实例:

from openai import OpenAI
 
client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

在 qwen-agent 里,用一个配置字典替代:

llm_cfg = {
    "model": "qwen-flash",
    "model_type": "qwen_dashscope",
    "api_key": api_key,
}

两者做的事情完全一样:指定模型 + 认证信息 + 端点地址。框架版多了一个 model_type,告诉框架"这个配置该走哪个协议"——仅此而已。

关键认知:Client 层不包含任何 Agent 逻辑。它就是一个带认证的 HTTP 客户端。所有支持 OpenAI 兼容接口的模型(千问、DeepSeek、Moonshot 等)都可以无缝切换,只改 base_urlapi_key


第二层:Tools — Agent 的手脚

职责:定义 Agent 可以调用的函数,告诉 LLM"我能做什么"。

每个工具需要两样东西:

  1. Python 函数 — 真正执行的逻辑
  2. JSON Schema — 告诉 LLM 函数名、描述、参数类型

原生版需要手写这两样并手动关联:

def check_destination_availability(destination: str) -> str:
    """检查度假目的地是否可预订。"""
    available = {"巴塞罗那": True, "东京": True, "巴黎": False}
    return f"{destination} {'可预订' if available.get(destination, False) else '不可预订'}"
 
# Schema 要单独手写
TOOLS_SCHEMA = [{
    "type": "function",
    "function": {
        "name": "check_destination_availability",
        "description": "检查度假目的地是否可预订。",
        "parameters": {
            "type": "object",
            "properties": {
                "destination": {
                    "type": "string",
                    "description": "要检查的目的地名称",
                },
            },
            "required": ["destination"],
        },
    },
}]
 
# 名字到函数的映射也是手动的
TOOL_MAP = {"check_destination_availability": check_destination_availability}

qwen-agent 用 @register_tool + BaseTool 把这三件事合并到一起:

@register_tool("check_destination_availability")
class CheckDestinationAvailability(BaseTool):
    description = "检查度假目的地是否可预订。"
    parameters = [
        {
            "name": "destination",
            "type": "string",
            "description": "要检查的目的地名称",
            "required": True,
        },
    ]
 
    def call(self, params: str, **kwargs) -> str:
        args = json.loads(params) if isinstance(params, str) else params
        destination = args.get("destination", "")
        available = {"巴塞罗那": True, "东京": True, "巴黎": False}
        return f"{destination} {'可预订' if available.get(destination, False) else '不可预订'}"

框架帮你做了:从 parameters 列表自动生成 JSON Schema,省去了手写 schema 和手动建立映射的两步。但本质上,传给 LLM 的还是同一份 JSON Schema。

关键认知:Tools 层是 Agent "能做事的范围"。你能做什么,取决于你注册了什么工具。这也是为什么在实际项目中,工具定义往往是最大量的代码——不是框架复杂,是你的业务逻辑复杂。


第三层:Agent — 把一切打包

职责:把 Client + 指令 + Tools 打包成一个可对话的对象。

原生版里,这层就是你手动维护的 messages 列表 + system_prompt + run_agent() 函数:

SYSTEM_PROMPT = "你是一个旅行预订代理。在推荐目的地之前,务必先检查可用性。"
 
def run_agent(user_message: str, messages: list[dict]) -> str:
    messages.append({"role": "user", "content": user_message})
    # ... tool calling 循环 ...
    return result

qwen-agent 提供了 Assistant 类来做这件事:

agent = Assistant(
    llm=llm_cfg,                          # Client 层
    name="TravelAvailabilityAgent",
    system_message=SYSTEM_PROMPT,          # 指令
    function_list=["check_destination_availability"],  # Tools 层
)

Assistant 就是 Client + 指令 + Tools 的"打包盒"。你不再需要手动维护 messages 列表和 while True 循环——这些都被封装在 agent.run() 里。

关键认知:Agent 层是框架的"用户体验"。Assistantcreate_agent()AgentExecutor——不同的框架有不同的类名,但它们的构造函数参数永远是三样东西:模型配置、系统指令、工具列表。


第四层:Session — 多轮记忆

职责:在多轮对话间保持上下文。

这是最容易被误解的一层。很多人以为框架的 session 是什么黑魔法,实际上:

Session 就是一个 messages 列表在多次调用间保持引用。

原生版没有任何魔法:

# 创建 session
session = [{"role": "system", "content": SYSTEM_PROMPT}]
 
# 第 1 轮
reply = run_agent("我想去巴黎,是否可以预订?", session)  # session 追加了 user/assistant/tool 消息
print(reply)
 
# 第 2 轮 — session 已经包含第 1 轮的所有上下文
reply = run_agent("我想去暖和的地方。有哪些可用的?", session)
print(reply)
 
# session 里共有多少条消息?
print(f"[Session 中共有 {len(session)} 条消息]")

qwen-agent 没有显式的 session 对象,但做法一样——把 messages 列表保留在外部,每次传给 agent.run()

session = []  # 就是一个空列表
 
# 第 1 轮
reply = run_turn("我想去巴黎,是否可以预订?", session)
 
# 第 2 轮 — session 已被 run_turn 更新
reply = run_turn("我想去暖和的地方。有哪些可用的?", session)

不同的框架对 session 的封装程度不同:

框架 Session 实现
原生 SDK messages = [],手动维护
qwen-agent messages = [],手动维护
MAF agent.create_session() 返回 AgentSession 对象
LangChain RunnableWithMessageHistory 包装器

但底层都一样——不断追加消息到同一个列表:[system, user, assistant, tool, assistant, user, assistant, ...]

关键认知:短期记忆 = 消息列表。长期记忆 = 外部存储(数据库/向量库)+ 工具。框架不提供"记忆能力"本身,它只提供管理消息列表的便利函数。


🔄 完整对照

把四层放在一起看:

层级 原生 SDK qwen-agent
Client OpenAI(base_url=..., api_key=...) llm_cfg = {"model_type": "qwen_dashscope"}
Tools TOOLS_SCHEMA 字典 + Python 函数 + TOOL_MAP @register_tool("name") + BaseTool
Agent SYSTEM_PROMPT + run_agent() 函数 Assistant(llm=..., system_message=..., function_list=[...])
Session messages = [] 列表 session = [] 列表

💡 三个关键认知

1. 框架不提供"Agent 能力",它只是组织代码的方式

Tool calling 能力来自 LLM API(所有主流模型都支持)。框架做的事是让你不需要手写 schema、映射、循环和消息管理。它"替你"做,而不是"替你提供"。

2. 四层抽象是通用的

不管你现在用 qwen-agent,明天换 LangChain,后天换 AutoGen——这四层永远在。变的只是类名和参数名,结构不变。你在原生版里学到的 messages.append(),在任何一个框架的 session 对象里都是一样的东西。

3. 理解层与层的边界,才知道出问题时该看哪里

  • LLM 没调用工具?→ 检查 Tools 层的 Schema 是否正确
  • Agent 忘记上下文?→ 检查 Session 层的 messages 列表是否正确追加
  • 连接报错?→ 检查 Client 层的 api_keybase_url
  • Agent 行为不符预期?→ 检查 Agent 层的 system_message

每个问题对应一层,不会在代码里瞎找。


🚀 运行起来

cd 02-explore-agentic-frameworks/code_samples
 
# 先手写,理解四层结构
python 02-python-agent-framework.py
 
# 再看框架,体会封装
python 02-qwen-agent-framework.py

🔮 下一篇

有了这四层的基础,下一篇将进入更实用的主题——Agentic 设计模式:怎么给 Agent 写好的指令?怎么让 Agent 返回结构化的 JSON?怎么把任务拆给多个 Agent?

所有代码和文章更新在 Building Agent from Scratch


✍️ 结语

学框架的正确顺序不是"先选框架再写代码",而是"先手写代码再读框架文档"。当你手写过四层抽象,再看任何一个框架的文档时,你看到的不再是神秘的 API,而是"哦,这一层就是我的 TOOLS_SCHEMA,那一层就是我的 messages 列表"。

框架是你能手写的代码的工程化封装。理解了这个,你就自由了。