构建自己的Agent框架
这章的重点是从"使用现成框架"转向"自己实现一个基础 Agent 框架"。这一章不再只是调用 AutoGen、AgentScope、CAMEL 或 LangGraph,而是尝试抽象出一个通用的 Agent 技术底座:统一 LLM 调用、统一消息格式、统一 Agent 接口、统一工具系统。
这样做的意义在于,后续无论是实现 ReAct、Reflection、Plan-and-Solve,还是接入搜索、计算、RAG、Memory,都不需要重新写一套代码,而是在统一接口上扩展。
1. 为什么要自建 Agent 框架
现成框架虽然功能强,但也存在一些问题:抽象层过厚、版本变化快、底层逻辑黑盒化、难以精确控制执行过程。对于学习 Agent 原理来说,如果只会调用框架 API,很容易知道"怎么用",但不清楚"为什么这样设计"。
因此,我们自己构建一个轻量级框架 HelloAgents。它不追求一开始就很复杂,而是先实现 Agent 系统最基础的几块能力:
text
LLM 接口
消息系统
配置管理
Agent 基类
Agent 范式实现
工具系统
2. LLM 接口概括
LLM 接口是整个 Agent 框架的底层入口。无论上层是 SimpleAgent、ReActAgent,还是 Plan-and-Solve,本质上都需要通过统一接口调用大语言模型。
本章构建了 HelloAgentsLLM,它的作用是屏蔽不同模型供应商的差异。也就是说,上层 Agent 不需要关心底层到底是 OpenAI、DeepSeek、ModelScope、Ollama,还是本地模型,只需要调用统一方法即可。
理想状态下,上层代码只写:
python
llm = HelloAgentsLLM()
response = llm.invoke(messages)
至于 API Key 从哪里读、base_url 是什么、provider 是谁,都交给 LLM 接口内部处理。
这种设计的价值是:模型可以替换,但 Agent 逻辑不需要大改。对于一个框架来说,这是非常关键的解耦。

3. 框架接口实现
这一节是第七章的核心基础。它主要实现三个文件:
text
message.py
config.py
agent.py
这三个组件分别解决三个问题:
text
Message:框架内部如何表示消息
Config:框架配置如何集中管理
Agent:所有智能体应该遵循什么统一接口
Message:统一消息格式
Agent 系统里最基本的数据就是消息。用户输入是一条消息,模型回复是一条消息,工具结果也是一条消息。如果消息格式不统一,后续做历史记录、上下文管理、工具调用、日志追踪都会很混乱。
因此,框架中设计了一个 Message 类:
python
from typing import Optional, Dict, Any, Literal
from datetime import datetime
from pydantic import BaseModel
MessageRole = Literal["user", "assistant", "system", "tool"]
class Message(BaseModel):
content: str
role: MessageRole
timestamp: datetime = None
metadata: Optional[Dict[str, Any]] = None
def __init__(self, content: str, role: MessageRole, **kwargs):
super().__init__(
content=content,
role=role,
timestamp=kwargs.get("timestamp", datetime.now()),
metadata=kwargs.get("metadata", {})
)
def to_dict(self) -> Dict[str, Any]:
return {
"role": self.role,
"content": self.content
}
这个类的重点不是复杂,而是规范。它规定了一条消息至少包含两个核心字段:
text
content:消息内容
role:消息角色
其中 role 被限制为四种:
text
user
assistant
system
tool
这样做有两个好处。
第一,和 OpenAI API 的消息格式保持兼容。通过 to_dict() 方法,框架内部的 Message 对象可以直接转成大模型接口需要的字典格式。
第二,为后续扩展留空间。timestamp 可以用于日志和调试,metadata 可以存储工具名、token 数量、来源信息、置信度等额外内容。
可以理解为:
text
Message = Agent 框架中的最小通信单位
它虽然简单,但后续的多轮对话、上下文工程、工具调用记录,都会依赖这个基础结构。
Config:统一配置管理
Config 类负责把框架运行时需要的配置集中管理起来。比如模型温度、最大 token 数、日志级别、历史消息长度等,都不应该散落在各个文件中。
示例代码如下:
python
class Config(BaseModel):
default_model: str = "gpt-3.5-turbo"
default_provider: str = "openai"
temperature: float = 0.7
max_tokens: Optional[int] = None
debug: bool = False
log_level: str = "INFO"
max_history_length: int = 100
@classmethod
def from_env(cls) -> "Config":
return cls(
debug=os.getenv("DEBUG", "false").lower() == "true",
log_level=os.getenv("LOG_LEVEL", "INFO"),
temperature=float(os.getenv("TEMPERATURE", "0.7")),
max_tokens=int(os.getenv("MAX_TOKENS")) if os.getenv("MAX_TOKENS") else None,
)
这个类体现的是"配置和代码分离"的思想。默认值保证框架可以直接运行,环境变量又允许用户在不同运行环境中调整配置。
例如开发环境可以设置:
text
DEBUG=true
TEMPERATURE=0.7
生产环境可以设置:
text
DEBUG=false
LOG_LEVEL=WARNING
这样就不需要改代码。
Config 的价值在项目变大之后会更明显。没有统一配置类时,参数会散落在 Agent、LLM、Tool、测试脚本中,后期很难维护。有了 Config 后,各个模块只需要依赖同一个配置对象。
Agent 抽象基类
Agent 基类是整个框架最重要的抽象。它规定了所有智能体都应该具备的共同结构。
python
from abc import ABC, abstractmethod
class Agent(ABC):
def __init__(
self,
name: str,
llm: HelloAgentsLLM,
system_prompt: Optional[str] = None,
config: Optional[Config] = None
):
self.name = name
self.llm = llm
self.system_prompt = system_prompt
self.config = config or Config()
self._history: list[Message] = []
@abstractmethod
def run(self, input_text: str, **kwargs) -> str:
pass
def add_message(self, message: Message):
self._history.append(message)
def clear_history(self):
self._history.clear()
def get_history(self) -> list[Message]:
return self._history.copy()
这个设计的关键在于:所有具体 Agent 都必须实现 run() 方法。
也就是说,无论是:
text
SimpleAgent
ReActAgent
ReflectionAgent
PlanAndSolveAgent
FunctionCallAgent
它们对外暴露的执行入口都是:
python
agent.run(input_text)
这样上层使用者不需要关心具体 Agent 内部怎么工作。SimpleAgent 可能只是直接问模型,ReActAgent 可能会多轮调用工具,ReflectionAgent 可能会自我反思多次,但它们对外都是统一接口。
这就是框架化的意义。
框架还在基类中统一管理历史记录:
text
add_message()
clear_history()
get_history()
这样每个 Agent 不需要重复实现对话历史管理。子类只需要专注自己的推理范式。
4 Agent 范式的框架化实现
这一节是在第四章已有 Agent 范式的基础上进行框架化重构。重点不是重新发明这些范式,而是把它们统一纳入 Agent 基类体系中。
框架化重构主要带来三个变化:
text
1. 统一初始化参数
2. 统一 run() 执行入口
3. 统一历史记录和工具接口
也就是说,之前每个 Agent 可能是独立脚本,现在它们都变成框架中的标准组件。
SimpleAgent:基础对话 Agent
SimpleAgent 是最基础的智能体。它的核心逻辑就是:接收用户输入,构造消息列表,调用 LLM,返回结果。
它还加入了两个扩展点:
text
可选工具调用
流式响应
python
from typing import Optional, Iterator
from hello_agents import SimpleAgent, HelloAgentsLLM, Config, Message
class MySimpleAgent(SimpleAgent):
"""
重写的简单对话Agent
展示如何基于框架基类构建自定义Agent
"""
def __init__(
self,
name: str,
llm: HelloAgentsLLM,
system_prompt: Optional[str] = None,
config: Optional[Config] = None,
tool_registry: Optional['ToolRegistry'] = None,
enable_tool_calling: bool = True
):
super().__init__(name, llm, system_prompt, config)
self.tool_registry = tool_registry
self.enable_tool_calling = enable_tool_calling and tool_registry is not None
print(f"✅ {name} 初始化完成,工具调用: {'启用' if self.enable_tool_calling else '禁用'}")

如果启用了工具调用,它会把可用工具描述加入系统提示词中,让模型知道当前可以使用哪些工具。例如:
text
你可以使用以下工具:
- calculator:执行数学计算
- search:执行网络搜索
然后约定工具调用格式:
text
[TOOL_CALL:tool_name:parameters]
当模型输出类似:
text
[TOOL_CALL:calculator:15 * 8 + 32]
Agent 就会解析出工具名和参数,调用对应工具,把工具结果重新放回对话,再让模型基于结果生成最终答案。
这说明 SimpleAgent 已经不只是"问答机器人",而是具备了基础的工具增强能力。
ReActAgent:推理与行动结合
ReActAgent 的核心是:
text
Thought → Action → Observation → Thought → ...
它让模型先思考,再选择行动。如果需要外部信息,就调用工具;如果信息足够,就输出最终答案。
框架化后的 ReActAgent 主要改进在两个方面。
第一,提示词格式更严格。它要求模型每轮都按照固定格式输出:
text
Thought: 分析当前问题
Action: 工具名[参数] 或 Finish[最终答案]
这样程序才能稳定解析模型输出。
第二,工具调用不再写死,而是统一通过 ToolRegistry 执行。
python
observation = self.tool_registry.execute_tool(tool_name, tool_input)
这意味着 ReActAgent 不需要知道具体有哪些工具,也不需要关心工具内部如何实现。它只负责决定"调用哪个工具、传入什么参数",真正执行由工具注册表完成。

ReActAgent 的基本流程可以理解为:
text
输入问题
↓
构建 ReAct Prompt
↓
LLM 输出 Thought / Action
↓
如果 Action 是工具调用:执行工具并记录 Observation
↓
继续下一轮
↓
如果 Action 是 Finish:返回最终答案
为了防止无限循环,框架中会设置 max_steps。如果模型一直不结束,达到最大步数后就返回失败提示。
ReflectionAgent:执行---反思---优化
ReflectionAgent 的核心思想是让模型先生成一个初始答案,然后再对自己的答案进行审查,最后根据反馈进行修改。
它的流程是:
text
Initial Answer
↓
Reflect
↓
Refine
↓
Final Answer
第七章中的框架化版本把提示词抽象成三类:
text
initial:生成初始答案
reflect:审查当前答案
refine:根据反馈改进答案
这种设计比写死代码更灵活。比如默认情况下,它可以用于文章生成、分析任务、总结任务;如果换成代码相关提示词,它就可以变成代码生成与代码审查 Agent。
例如:
python
code_prompts = {
"initial": "你是Python专家,请编写函数:{task}",
"reflect": "请审查代码的算法效率:\n任务:{task}\n代码:{content}",
"refine": "请根据反馈优化代码:\n任务:{task}\n反馈:{feedback}"
}
这说明框架化后的 ReflectionAgent 不再绑定某一个具体任务,而是可以通过自定义 prompt 适配不同场景。

它适合用于:
text
文本润色
代码优化
方案改进
论文摘要修订
回答质量提升
PlanAndSolveAgent:先规划再执行
Plan-and-Solve 的核心思想是:复杂任务不要直接回答,先拆解成步骤,再逐步解决。
流程是:
text
用户问题
↓
Planner 生成计划
↓
Executor 逐步执行每个子任务
↓
汇总最终答案
框架化版本的一个重要改进是:要求 Planner 输出 Python 列表格式。
例如:
python
["计算周一销量", "计算周二销量", "计算周三销量", "求三天总和"]
这样程序可以稳定解析计划,而不是从一段自然语言里猜测步骤。
执行阶段则由 Executor 根据当前步骤、历史结果和原始问题生成该步骤的答案。它不是一次性解决整个问题,而是逐步完成。

这个范式适合处理:
text
数学应用题
复杂推理题
多步骤分析任务
项目规划
实验方案设计
它的优势是过程更清楚,但也依赖 Planner 的计划质量。如果第一步计划拆错,后面执行也会被带偏。
FunctionCallAgent:原生函数调用 Agent
FunctionCallAgent 是基于 OpenAI 原生 function calling 机制实现的 Agent。它和 ReActAgent 最大的区别是:ReActAgent 通过提示词约束模型输出工具调用格式,而 FunctionCallAgent 使用模型原生的工具调用能力。
两者可以简单对比:
text
ReActAgent:
依赖 prompt 格式解析,例如 工具名[参数]
FunctionCallAgent:
依赖模型原生 tools / function calling schema
FunctionCallAgent 会把工具转换成 OpenAI function calling schema,然后交给模型决定是否调用工具。这样比纯 prompt 解析更稳,因为参数通常是结构化 JSON,不容易出现格式错乱。
它适合用于支持原生工具调用的大模型。如果模型本身支持 function calling,那么这种方式通常比手写工具调用格式更可靠。
5. 工具系统
工具系统是 Agent 能力扩展的关键。LLM 本身只擅长语言理解和生成,但它不能直接计算、搜索、访问数据库或调用外部系统。工具系统的作用就是让 Agent 具备"行动能力"。
第七章中的工具系统主要包括四层:
text
Tool 抽象基类
ToolParameter 参数定义
ToolRegistry 工具注册表
高级工具机制:工具链、异步执行、多源搜索
Tool 基类:统一工具接口
工具系统首先需要一个统一的抽象。所有工具都应该有相同的基本接口,这样 Agent 才能以统一方式调用它们。
python
class Tool(ABC):
def __init__(self, name: str, description: str):
self.name = name
self.description = description
@abstractmethod
def run(self, parameters: Dict[str, Any]) -> str:
pass
@abstractmethod
def get_parameters(self) -> List[ToolParameter]:
pass
这个设计的重点是两个方法:
text
run():执行工具
get_parameters():描述工具需要哪些参数
run() 让所有工具都有统一执行入口。无论是计算器、搜索工具、文件读取工具,还是数据库查询工具,对 Agent 来说都是:
python
tool.run(parameters)
get_parameters() 则让工具具备自描述能力。Agent 或框架可以通过这个方法知道工具需要什么参数、参数类型是什么、是否必填。
ToolParameter:工具参数描述
ToolParameter 用来描述工具参数。
python
class ToolParameter(BaseModel):
name: str
type: str
description: str
required: bool = True
default: Any = None
它解决的问题是:工具不能只告诉 Agent "我叫 calculator",还应该告诉 Agent "我需要一个 expression 参数,它是字符串类型,用来表示数学表达式"。
例如计算器工具可能需要:
text
name: expression
type: string
description: 要计算的数学表达式
required: true
这样做的好处是:
text
参数更清楚
更容易做类型校验
更容易生成工具文档
更容易转换成 function calling schema
这也是工具系统走向工程化的重要一步。
ToolRegistry:工具注册与发现
ToolRegistry 是工具系统的管理中心。它负责注册工具、查找工具、执行工具、生成工具描述。
它支持两种注册方式。
第一种是注册完整 Tool 对象,适合复杂工具:
python
registry.register_tool(calculator_tool)
第二种是直接注册普通函数,适合简单工具:
python
registry.register_function(
name="search",
description="搜索信息",
func=search_function
)
这两种方式都很有用。
完整 Tool 对象更规范,适合需要参数定义、校验、状态维护的工具。普通函数注册更轻量,适合快速把已有函数接入框架。
ToolRegistry 还有一个非常重要的方法:
python
get_tools_description()
它会把所有工具整理成描述文本,然后放入 Agent 的提示词中。这样模型才知道自己有哪些可用工具。
例如:
text
可用工具:
- calculator:执行数学计算
- search:搜索互联网信息
对于基于 prompt 的工具调用来说,这一步非常关键。模型只有看到工具说明,才可能正确选择工具。
OpenAI Schema 转换
为了支持 FunctionCallAgent,工具系统还需要能把工具定义转换为 OpenAI function calling schema。
也就是说,工具不仅要能被 prompt 描述,还要能被结构化地交给模型。
转换后的 schema 大致包含:
text
工具名称
工具描述
参数 properties
必填参数 required
这样模型就可以按照结构化参数调用工具,而不是在自然语言里写一个不稳定的工具调用字符串。
这也是工具系统从"提示词调用"走向"原生函数调用"的关键。
自定义工具:计算器与高级搜索
第七章中还展示了如何实现自定义工具。计算器工具适合演示基础工具开发,因为它输入简单、结果明确。
更复杂的是高级搜索工具。它把多个搜索源整合到一个工具中,例如 Tavily 和 SerpAPI。工具内部会按顺序尝试不同搜索源,如果一个失败,就继续尝试下一个。
这种设计体现了工具系统的工程化思维:
text
多个后端
自动降级
异常捕获
结果统一返回
Agent 不需要知道底层使用了哪个搜索服务。它只需要调用:
text
advanced_search
具体是 Tavily 返回结果,还是 SerpAPI 返回结果,由工具内部决定。
这就是封装的价值。
工具链:多个工具顺序执行
有些任务不是一个工具能完成的,而是需要多个工具串联。
例如:
text
搜索资料 → 提取关键信息 → 计算结果 → 生成总结
第七章中设计了 ToolChain,用于把多个工具按步骤组织起来。每一步可以指定:
text
tool_name:调用哪个工具
input_template:输入模板
output_key:把结果保存到哪个变量
执行时,工具链会维护一个上下文 context,前一步的输出可以被后一步使用。
例如:
text
第 1 步:search("{input}") → search_result
第 2 步:calculator("根据 {search_result} 计算") → calculation_result
这种设计已经很接近 LangGraph 中的节点流转思想,只不过这里是工具层面的顺序编排。
异步工具执行
有些工具很耗时,比如网络搜索、数据库查询、文件解析。如果顺序执行多个工具,整体速度会很慢。
因此,第七章还实现了异步工具执行器 AsyncToolExecutor。它通过线程池并行执行多个工具任务。
适合并行的场景包括:
text
同时搜索多个关键词
同时调用多个数据源
同时处理多个文件
同时执行多个互不依赖的计算任务
这种设计可以明显提高复杂 Agent 的响应速度。但前提是任务之间没有强依赖。如果后一个工具必须使用前一个工具的输出,就不能简单并行,而应该使用工具链顺序执行。