大模型应用开发实战(17)——从 0 到 1 构建你的智能体(Agent)框架

大模型应用开发实战(17)——从 0 到 1 构建你的智能体(Agent)框架blockquote 个人主页 小李同学 LSH 的主页 作者简介 LLM 学习者 希望大家多多支持 我们一起进步 如果文章对你有帮助的话 欢迎评论 点赞 收藏 加关注 目录 一 为什么很多 Agent Demo 很快就写不下去了 二 HelloAgents 框架的设计理念 三 Agent 框架扩展实现 增加对 ModelScope 平台的支持 blockquote

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



 
  
    
    

🤵‍♂️ 个人主页:小李同学_LSH的主页

目录

一、为什么很多 Agent Demo 很快就写不下去了

二、HelloAgents框架的设计理念

三、Agent框架扩展实现

增加对 ModelScope 平台的支持

(1)创建自定义LLM类并继承

(2)重写 init 方法以支持新供应商

(3)使用自定义的 MyLLM 类

本地模型调用

VLLM

Ollama

接入 HelloAgentsLLM

自动检测机制

四、​​​​​​框架接口实现

Message 类

​​​​​​Config 类

​​​​​​Agent 抽象基类


本文实现对Agents框架的服务器扩展与接口实现。

很多人第一次做 Agent,思路都差不多:接一个模型,写一段 Prompt,绑两个工具,然后跑起来。演示的时候看着很顺,真到稍微复杂一点的任务,问题马上就出来了:状态乱了、上下文爆了、工具越来越难管、报错也不知道该从哪修。

这时候你会发现,真正限制 Agent 上限的,往往不是模型本身,而是你有没有一套像样的框架。


Datawhale 的 Hello-Agents 在第七章开始正式从“使用框架”切到“自己搭框架”的视角,核心原因说得很直接:现有框架常常存在抽象过重、迭代太快、黑盒化严重、依赖复杂等问题;而自建框架的价值,在于能真正理解 Agent 原理、拿到完整控制权、并练到系统设计能力。

明确给出了一个三层结构:核心框架层、Agent 实现层、工具系统层,并强调“除了核心 Agent 类,一切皆可视作工具”的统一抽象。

这篇文章,我不打算停在“框架是什么”这种概念层,而是直接从工程视角出发,带你从 0 到 1 搭一个最小可用、后续还能扩展的 Agent 框架。你看完之后,至少会明白三件事:

  1. 为什么很多 Agent Demo 一上复杂任务就散架。
  2. 一个能长期维护的 Agent 框架,最少应该包含哪些模块。
  3. 怎么从“能跑”升级到“能扩、能调、能排错”。

参考DataWhale的Hello-Agents框架https://hello-agents.datawhale.cc/#/./chapter7/%E7%AC%AC%E4%B8%83%E7%AB%A0%20%E6%9E%84%E5%BB%BA%E4%BD%A0%E7%9A%84Agent%E6%A1%86%E6%9E%B6?id=_712-helloagents%e6%a1%86%e6%9e%b6%e7%9a%84%e8%ae%be%e8%ae%a1%e7%90%86%e5%bf%b5

hello-agents/ ├── hello_agents/ │ │ │ ├── core/ # 核心框架层 │ │ ├── agent.py # Agent基类 │ │ ├── llm.py # HelloAgentsLLM统一接口 │ │ ├── message.py # 消息系统 │ │ ├── config.py # 配置管理 │ │ └── exceptions.py # 异常体系 │ │ │ ├── agents/ # Agent实现层 │ │ ├── simple_agent.py # SimpleAgent实现 │ │ ├── react_agent.py # ReActAgent实现 │ │ ├── reflection_agent.py # ReflectionAgent实现 │ │ └── plan_solve_agent.py # PlanAndSolveAgent实现 │ │ │ ├── tools/ # 工具系统层 │ │ ├── base.py # 工具基类 │ │ ├── registry.py # 工具注册机制 │ │ ├── chain.py # 工具链管理系统 │ │ ├── async_executor.py # 异步工具执行器 │ │ └── builtin/ # 内置工具集 │ │ ├── calculator.py # 计算工具 │ │ └── search.py # 搜索工具 └──

HelloAgents的架构设计遵循了“分层解耦、职责单一、接口统一”的核心原则

大多数 Demo 不是“错”,而是“短命”。

  • 能不能调用天气工具
  • 能不能调用搜索
  • 能不能做一次 ReAct 推理
  • 能不能让模型按 JSON 输出

这些事情单独看都不难,但一旦你把它们放进同一个系统里,问题就开始叠加:

  • 同一个会话里消息历史越来越长,模型逐渐失控
  • 工具变多后,函数签名和参数校验越来越乱
  • 失败重试没有统一入口,异常处理全靠 try/except
  • 不同 Agent 范式混在一起,代码耦合严重
  • 配置写死在代码里,后面切模型、切 provider、切温度都很痛苦


也就是说,很多人以为自己在做 Agent,实际上只是在不断给一个脚本打补丁。

而框架的意义,不是为了“高级”,而是为了把变化隔离开,把复杂性收住

用户输入 ↓ Agent 接收消息 ↓ 调用 LLM ↓ 模型判断:直接回答 or 调用工具 ↓ 如果调用工具 -> 执行工具 -> 返回结果 ↓ 把工具结果追加进消息历史 ↓ 再次调用模型 ↓ 输出最终答案

构建一个新的Agent框架,关键不在于功能的多少,而在于设计理念是否能真正解决现有框架的痛点。HelloAgents框架的设计围绕着一个核心问题展开:如何让学习者既能快速上手,又能深入理解Agent的工作原理?

(1)轻量级与教学友好的平衡

一个优秀的学习框架应该具备完整的可读性。HelloAgents将核心代码按照章节区分开,这是基于一个简单的原则:任何有一定编程基础的开发者都应该能够在合理的时间内完全理解框架的工作原理。在依赖管理方面,框架采用了极简主义的策略。除了OpenAI的官方SDK和几个必要的基础库外,不引入任何重型依赖。如果遇到问题时,我们可以直接定位到框架本身的代码,而不需要在复杂的依赖关系中寻找答案。

(2)基于标准API的务实选择

OpenAI的API已经成为了行业标准,几乎所有主流的LLM提供商都在努力兼容这套接口。HelloAgents选择在这个标准之上构建,而不是重新发明一套抽象接口。这个决定主要是出于几点动机。首先是兼容性的保证,当你掌握了HelloAgents的使用方法后,迁移到其他框架或将其集成到现有项目中时,底层的API调用逻辑是完全一致的。其次是学习成本的降低。你不需要学习新的概念模型,因为所有的操作都基于你已经熟悉的标准接口。

(3)渐进式学习路径的精心设计

HelloAgents提供了一条清晰的学习路径。我们将会把每一章的学习代码,保存为一个可以pip下载的历史版本,因此无需担心代码的使用成本,因为每一个核心的功能都将会是你自己编写的。这种设计让你能够按照自己的需求和节奏前进。每一步的升级都是自然而然的,不会产生概念上的跳跃或理解上的断层。值得一提的是,我们这一章的内容,也是基于前六章的内容来完善的。同样,这一章也是为后续高级知识学习部分打下框架基础。

(4)统一的“工具”抽象:万物皆为工具

为了彻底贯彻轻量级与教学友好的理念,HelloAgents在架构上做出了一个关键的简化:除了核心的Agent类,一切皆为Tools。在许多其他框架中需要独立学习的Memory(记忆)、RAG(检索增强生成)、RL(强化学习)、MCP(协议)等模块,在HelloAgents中都被统一抽象为一种“工具”。这种设计的初衷是消除不必要的抽象层,让学习者可以回归到最直观的“智能体调用工具”这一核心逻辑上,从而真正实现快速上手和深入理解的统一。

增加对 ModelScope 平台的支持
(1)创建自定义LLM类并继承

通过继承 HelloAgentsLLM,来增加对 ModelScope 平台的支持。

假设我们的项目目录中有一个 my_llm.py 文件。我们首先从 hello_agents 库中导入 HelloAgentsLLM 基类,然后创建一个名为 MyLLM 的新类继承它。

# my_llm.py import os from typing import Optional from openai import OpenAI from hello_agents import HelloAgentsLLM class MyLLM(HelloAgentsLLM): """ 一个自定义的LLM客户端,通过继承增加了对ModelScope的支持。 """ pass # 暂时留空 
(2)重写 __init__ 方法以支持新供应商

接下来,我们在 MyLLM 类中重写 __init__ 方法。我们的目标是:当用户传入 provider="modelscope" 时,执行我们自定义的逻辑;否则,就调用父类 HelloAgentsLLM 的原始逻辑,使其能够继续支持 OpenAI 等其他内置的供应商。

class MyLLM(HelloAgentsLLM): def __init__( self, model: Optional[str] = None, api_key: Optional[str] = None, base_url: Optional[str] = None, provider: Optional[str] = "auto", kwargs ): # 检查provider是否为我们想处理的'modelscope' if provider == "modelscope": print("正在使用自定义的 ModelScope Provider") self.provider = "modelscope" # 解析 ModelScope 的凭证 self.api_key = api_key or os.getenv("MODELSCOPE_API_KEY") self.base_url = base_url or "https://api-inference.modelscope.cn/v1/" # 验证凭证是否存在 if not self.api_key: raise ValueError("ModelScope API key not found. Please set MODELSCOPE_API_KEY environment variable.") # 设置默认模型和其他参数 self.model = model or os.getenv("LLM_MODEL_ID") or "Qwen/Qwen2.5-VL-72B-Instruct" self.temperature = kwargs.get('temperature', 0.7) self.max_tokens = kwargs.get('max_tokens') self.timeout = kwargs.get('timeout', 60) # 使用获取的参数创建OpenAI客户端实例 self._client = OpenAI(api_key=self.api_key, base_url=self.base_url, timeout=self.timeout) else: # 如果不是 modelscope, 则完全使用父类的原始逻辑来处理 super().__init__(model=model, api_key=api_key, base_url=base_url, provider=provider, kwargs) 

这段代码展示了“重写”的思想:拦截了 provider="modelscope" 的情况并进行了特殊处理,对于其他所有情况,则通过 super().__init__(...) 交还给父类,保留了原有框架的全部功能。

(3)使用自定义的 MyLLM 类

现在,我们可以在项目的业务逻辑中,像使用原生 HelloAgentsLLM 一样使用我们自己的 MyLLM 类。

首先,在 .env 文件中配置 ModelScope 的 API 密钥:

# .env file MODELSCOPE_API_KEY="your-modelscope-api-key" 

然后,在主程序中导入并使用 MyLLM

# my_main.py from dotenv import load_dotenv from my_llm import MyLLM # 注意:这里导入我们自己的类 # 加载环境变量 load_dotenv() # 实例化我们重写的客户端,并指定provider llm = MyLLM(provider="modelscope") # 准备消息 messages = [{"role": "user", "content": "你好,请介绍一下你自己。"}] # 发起调用,think等方法都已从父类继承,无需重写 response_stream = llm.think(messages) # 打印响应 print("ModelScope Response:") for chunk in response_stream: # chunk在my_llm库中已经打印过一遍,这里只需要pass即可 # print(chunk, end="", flush=True) pass 

通过以上步骤,我们就在不修改 hello-agents 库源码的前提下,成功为其扩展了新的功能。这种方法不仅保证了代码的整洁和可维护性,也使得未来升级 hello-agents 库时,我们的定制化功能不会丢失。

本地模型调用

使用 Hugging Face Transformers 库在本地直接运行开源模型。该方法非常适合入门学习和功能验证,但其底层实现在处理高并发请求时性能有限,通常不作为生产环境的首选方案。


为了在本地实现高性能、生产级的模型推理服务,社区涌现出了 VLLM 和 Ollama 等优秀工具。它们通过连续批处理、PagedAttention 等技术,显著提升了模型的吞吐量和运行效率,并将模型封装为兼容 OpenAI 标准的 API 服务。这意味着,我们可以将它们无缝地集成到 HelloAgentsLLM 中。

VLLM

VLLM 是一个为 LLM 推理设计的高性能 Python 库。它通过 PagedAttention 等先进技术,可以实现比标准 Transformers 实现高出数倍的吞吐量。

下面是在本地部署一个 VLLM 服务的完整步骤:

首先,需要根据你的硬件环境(特别是 CUDA 版本)安装 VLLM。推荐遵循其官方文档进行安装,以避免版本不匹配问题。

pip install vllm 

安装完成后,使用以下命令即可启动一个兼容 OpenAI 的 API 服务。VLLM 会自动从 Hugging Face Hub 下载指定的模型权重(如果本地不存在)。我们依然以 Qwen1.5-0.5B-Chat 模型为例:

# 启动 VLLM 服务,并加载 Qwen1.5-0.5B-Chat 模型 python -m vllm.entrypoints.openai.api_server --model Qwen/Qwen1.5-0.5B-Chat --host 0.0.0.0 --port 8000 

服务启动后,便会在 http://localhost:8000/v1 地址上提供与 OpenAI 兼容的 API。

Ollama

Ollama 进一步简化了本地模型的管理和部署,它将模型下载、配置和服务启动等步骤封装到了一条命令中,非常适合快速上手。

访问 Ollama 官方网站下载并安装适用于你操作系统的客户端。

安装后,打开终端,执行以下命令即可下载并运行一个模型(以 Llama 3 为例)。Ollama 会自动处理模型的下载、服务封装和硬件加速配置。

# 首次运行会自动下载模型,之后会直接启动服务 ollama run llama3 

当你在终端看到模型的交互提示时,即表示服务已经成功在后台启动。Ollama 默认会在 http://localhost:11434/v1 地址上暴露 OpenAI 兼容的 API 接口。

接入 HelloAgentsLLM

由于 VLLM 和 Ollama 都遵循了行业标准 API,将它们接入 HelloAgentsLLM 的过程非常简单。我们只需在实例化客户端时,将它们视为一个新的 provider 即可。

例如,连接本地运行的 VLLM 服务:

llm_client = HelloAgentsLLM( provider="vllm", model="Qwen/Qwen1.5-0.5B-Chat", # 需与服务启动时指定的模型一致 base_url="http://localhost:8000/v1", api_key="vllm" # 本地服务通常不需要真实API Key,可填任意非空字符串 ) 

或者,通过设置环境变量并让客户端自动检测,实现代码的零修改:

# 在 .env 文件中设置 LLM_BASE_URL="http://localhost:8000/v1" LLM_API_KEY="vllm" # Python 代码中直接实例化即可 llm_client = HelloAgentsLLM() # 将自动检测为 vllm 

同理,连接本地的 Ollama 服务也一样简单:

llm_client = HelloAgentsLLM( provider="ollama", model="llama3", # 需与 `ollama run` 指定的模型一致 base_url="http://localhost:11434/v1", api_key="ollama" # 本地服务同样不需要真实 Key ) 
自动检测机制

为了尽可能减少用户的配置负担并遵循“约定优于配置”的原则,HelloAgentsLLM 内部设计了两个核心辅助方法:_auto_detect_provider 和 _resolve_credentials

它们协同工作,_auto_detect_provider 负责根据环境信息推断服务商,而 _resolve_credentials 则根据推断结果完成具体的参数配置。

_auto_detect_provider 方法负责根据环境信息,按照下述优先级顺序,尝试自动推断服务商:

  1. 最高优先级:检查特定服务商的环境变量 这是最直接、最可靠的判断依据。框架会依次检查 MODELSCOPE_API_KEYOPENAI_API_KEYZHIPU_API_KEY 等环境变量是否存在。一旦发现任何一个,就会立即确定对应的服务商。
  2. 次高优先级:根据 base_url 进行判断 如果用户没有设置特定服务商的密钥,但设置了通用的 LLM_BASE_URL,框架会转而解析这个 URL。
    • 域名匹配:通过检查 URL 中是否包含 "api-inference.modelscope.cn""api.openai.com" 等特征字符串来识别云服务商。
    • 端口匹配:通过检查 URL 中是否包含 :11434 (Ollama), :8000 (VLLM) 等本地服务的标准端口来识别本地部署方案。
  3. 辅助判断:分析 API 密钥的格式 在某些情况下,如果上述两种方式都无法确定,框架会尝试分析通用环境变量 LLM_API_KEY 的格式。例如,某些服务商的 API 密钥有固定的前缀或独特的编码格式。不过,由于这种方式可能存在模糊性(例如多个服务商的密钥格式相似),因此它的优先级较低,仅作为辅助手段。

其部分关键代码如下:

def _auto_detect_provider(self, api_key: Optional[str], base_url: Optional[str]) -> str: """ 自动检测LLM提供商 """ # 1. 检查特定提供商的环境变量 (最高优先级) if os.getenv("MODELSCOPE_API_KEY"): return "modelscope" if os.getenv("OPENAI_API_KEY"): return "openai" if os.getenv("ZHIPU_API_KEY"): return "zhipu" # ... 其他服务商的环境变量检查 # 获取通用的环境变量 actual_api_key = api_key or os.getenv("LLM_API_KEY") actual_base_url = base_url or os.getenv("LLM_BASE_URL") # 2. 根据 base_url 判断 if actual_base_url: base_url_lower = actual_base_url.lower() if "api-inference.modelscope.cn" in base_url_lower: return "modelscope" if "open.bigmodel.cn" in base_url_lower: return "zhipu" if "localhost" in base_url_lower or "127.0.0.1" in base_url_lower: if ":11434" in base_url_lower: return "ollama" if ":8000" in base_url_lower: return "vllm" return "local" # 其他本地端口 # 3. 根据 API 密钥格式辅助判断 if actual_api_key: if actual_api_key.startswith("ms-"): return "modelscope" # ... 其他密钥格式判断 # 4. 默认返回 'auto',使用通用配置 return "auto" 

一旦 provider 被确定(无论是用户指定还是自动检测),_resolve_credentials 方法便会接手处理服务商的差异化配置。它会根据 provider 的值,去主动查找对应的环境变量,并为其设置默认的 base_url。其部分关键实现如下:

def _resolve_credentials(self, api_key: Optional[str], base_url: Optional[str]) -> tuple[str, str]: """根据provider解析API密钥和base_url""" if self.provider == "openai": resolved_api_key = api_key or os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY") resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api.openai.com/v1" return resolved_api_key, resolved_base_url elif self.provider == "modelscope": resolved_api_key = api_key or os.getenv("MODELSCOPE_API_KEY") or os.getenv("LLM_API_KEY") resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api-inference.modelscope.cn/v1/" return resolved_api_key, resolved_base_url # ... 其他服务商的逻辑 

个简单的例子来感受自动检测带来的便利。假设一个用户想要使用本地的 Ollama 服务,他只需在 .env 文件中进行如下配置:

LLM_BASE_URL="http://localhost:11434/v1" LLM_MODEL_ID="llama3" 

他完全不需要配置 LLM_API_KEY 或在代码中指定 provider。然后,在 Python 代码中,他只需简单地实例化 HelloAgentsLLM 即可:

from dotenv import load_dotenv from hello_agents import HelloAgentsLLM load_dotenv() # 无需传入 provider,框架会自动检测 llm = HelloAgentsLLM() # 框架内部日志会显示检测到 provider 为 'ollama' # 后续调用方式完全不变 messages = [{"role": "user", "content": "你好!"}] for chunk in llm.think(messages): print(chunk, end="") 

我们构建了 HelloAgentsLLM 这一核心组件,解决了与大语言模型通信的关键问题。不过它还需要一系列配套的接口和组件来处理数据流、管理配置、应对异常,并为上层应用的构建提供一个清晰、统一的结构。本节将讲述以下三个核心文件:

  • message.py: 定义了框架内统一的消息格式,确保了智能体与模型之间信息传递的标准化。
  • config.py: 提供了一个中心化的配置管理方案,使框架的行为易于调整和扩展。
  • agent.py: 定义了所有智能体的抽象基类(Agent),为后续实现不同类型的智能体提供了统一的接口和规范。
Message 类

在智能体与大语言模型的交互中,对话历史是至关重要的上下文。

“”“消息系统”“” 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]: """转换为字典格式(OpenAI API格式)""" return { "role": self.role, "content": self.content } def __str__(self) -> str: return f"[{self.role}] {self.content}"

该类的设计有几个关键点。首先,我们通过 typing.Literal 将 role 字段的取值严格限制为

 “user”, “assistant”, “system”, “tool”

 四种,这直接对应 OpenAI API 的规范,保证了类型安全。

除了 content 和 role 这两个核心字段外,我们还增加了 timestamp 和 metadata,为日志记录和未来功能扩展预留了空间。最后,to_dict() 方法是其核心功能之一,负责将内部使用的 Message 对象转换为与 OpenAI API 兼容的字典格式,体现了“对内丰富,对外兼容”的设计原则。

​​​​​​Config 类

Config 类的职责是将代码中硬编码配置参数集中起来,并支持从环境变量中读取。

“”“配置管理”“” import os from typing import Optional, Dict, Any from pydantic import BaseModel

class Config(BaseModel):

"""HelloAgents配置类""" # LLM配置 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, ) def to_dict(self) -> Dict[str, Any]: """转换为字典""" return self.dict()

将配置项按逻辑划分为 LLM配置系统配置 

每个配置项都设有合理的默认值,保证了框架在零配置下也能工作。

最核心的是 from_env() 类方法,它允许用户通过设置环境变量来覆盖默认配置,无需修改代码,这在部署到不同环境时尤其有用。

​​​​​​Agent 抽象基类

Agent 类是框架的顶层抽象。它定义了智能体应该具备的通用行为和属性,不关心具体的实现方式

通过 Python 的 abc (Abstract Base Classes) 模块,这强制智能体实现都必须遵循同一个“接口”。

“”“Agent基类”“” from abc import ABC, abstractmethod from typing import Optional, Any from .message import Message from .llm import HelloAgentsLLM from .config import Config

class Agent(ABC):

"""Agent基类""" 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: """运行Agent""" 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() def __str__(self) -> str: return f"Agent(name={self.name}, provider={self.llm.provider})" 

通过继承 ABC 被定义为一个不能直接实例化的抽象类。

构造函数 init 清晰地定义了 Agent 的核心依赖:名称、LLM 实例、系统提示词和配置。

使用 @abstractmethod 装饰的 run 方法,它强制所有子类必须实现此方法,从而保证了所有智能体都有统一的执行入口。此外,基类还提供了通用的历史记录管理方法,这些方法与 Message 类协同工作,体现了组件间的联系。

小讯
上一篇 2026-04-18 18:19
下一篇 2026-04-18 18:17

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/269754.html