♂️ 个人主页小李同学_LSH的主页✍ 作者简介LLM学习者 希望大家多多支持我们一起进步如果文章对你有帮助的话欢迎评论 点赞 收藏 加关注目录一、为什么很多 Agent Demo 很快就写不下去了二、HelloAgents框架的设计理念三、Agent框架扩展实现增加对 ModelScope 平台的支持1创建自定义LLM类并继承2重写 __init__ 方法以支持新供应商3使用自定义的 MyLLM 类本地模型调用VLLMOllama接入 HelloAgentsLLM自动检测机制四、框架接口实现Message 类Config 类Agent 抽象基类本文实现对Agents框架的服务器扩展与接口实现。很多人第一次做 Agent思路都差不多接一个模型写一段 Prompt绑两个工具然后跑起来。演示的时候看着很顺真到稍微复杂一点的任务问题马上就出来了状态乱了、上下文爆了、工具越来越难管、报错也不知道该从哪修。这时候你会发现真正限制 Agent 上限的往往不是模型本身而是你有没有一套像样的框架。Datawhale 的 Hello-Agents 在第七章开始正式从“使用框架”切到“自己搭框架”的视角核心原因说得很直接现有框架常常存在抽象过重、迭代太快、黑盒化严重、依赖复杂等问题而自建框架的价值在于能真正理解 Agent 原理、拿到完整控制权、并练到系统设计能力。明确给出了一个三层结构核心框架层、Agent 实现层、工具系统层并强调“除了核心 Agent 类一切皆可视作工具”的统一抽象。这篇文章我不打算停在“框架是什么”这种概念层而是直接从工程视角出发带你从 0 到 1 搭一个最小可用、后续还能扩展的 Agent 框架。你看完之后至少会明白三件事为什么很多 Agent Demo 一上复杂任务就散架。一个能长期维护的 Agent 框架最少应该包含哪些模块。怎么从“能跑”升级到“能扩、能调、能排错”。参考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%b5hello-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的架构设计遵循了分层解耦、职责单一、接口统一的核心原则一、为什么很多 Agent Demo 很快就写不下去了大多数 Demo 不是“错”而是“短命”。因为 Demo 的目标通常只有一个证明某个能力能跑通。比如能不能调用天气工具能不能调用搜索能不能做一次 ReAct 推理能不能让模型按 JSON 输出这些事情单独看都不难但一旦你把它们放进同一个系统里问题就开始叠加同一个会话里消息历史越来越长模型逐渐失控工具变多后函数签名和参数校验越来越乱失败重试没有统一入口异常处理全靠try/except不同 Agent 范式混在一起代码耦合严重配置写死在代码里后面切模型、切 provider、切温度都很痛苦也就是说很多人以为自己在做 Agent实际上只是在不断给一个脚本打补丁。而框架的意义不是为了“高级”而是为了把变化隔离开把复杂性收住。二、HelloAgents框架的设计理念用户输入 ↓ 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中都被统一抽象为一种“工具”。这种设计的初衷是消除不必要的抽象层让学习者可以回归到最直观的“智能体调用工具”这一核心逻辑上从而真正实现快速上手和深入理解的统一。三、Agent框架扩展实现增加对 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__方法。我们的目标是当用户传入providermodelscope时执行我们自定义的逻辑否则就调用父类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_keyself.api_key, base_urlself.base_url, timeoutself.timeout) else: # 如果不是 modelscope, 则完全使用父类的原始逻辑来处理 super().__init__(modelmodel, api_keyapi_key, base_urlbase_url, providerprovider, **kwargs)这段代码展示了“重写”的思想拦截了providermodelscope的情况并进行了特殊处理对于其他所有情况则通过super().__init__(...)交还给父类保留了原有框架的全部功能。3使用自定义的MyLLM类现在我们可以在项目的业务逻辑中像使用原生HelloAgentsLLM一样使用我们自己的MyLLM类。首先在.env文件中配置 ModelScope 的 API 密钥# .env file MODELSCOPE_API_KEYyour-modelscope-api-key然后在主程序中导入并使用MyLLM# my_main.py from dotenv import load_dotenv from my_llm import MyLLM # 注意:这里导入我们自己的类 # 加载环境变量 load_dotenv() # 实例化我们重写的客户端并指定provider llm MyLLM(providermodelscope) # 准备消息 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, flushTrue) pass通过以上步骤我们就在不修改hello-agents库源码的前提下成功为其扩展了新的功能。这种方法不仅保证了代码的整洁和可维护性也使得未来升级hello-agents库时我们的定制化功能不会丢失。本地模型调用使用 Hugging Face Transformers 库在本地直接运行开源模型。该方法非常适合入门学习和功能验证但其底层实现在处理高并发请求时性能有限通常不作为生产环境的首选方案。为了在本地实现高性能、生产级的模型推理服务社区涌现出了 VLLM 和 Ollama 等优秀工具。它们通过连续批处理、PagedAttention 等技术显著提升了模型的吞吐量和运行效率并将模型封装为兼容 OpenAI 标准的 API 服务。这意味着我们可以将它们无缝地集成到HelloAgentsLLM中。VLLMVLLM 是一个为 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。OllamaOllama 进一步简化了本地模型的管理和部署它将模型下载、配置和服务启动等步骤封装到了一条命令中非常适合快速上手。访问 Ollama 官方网站下载并安装适用于你操作系统的客户端。安装后打开终端执行以下命令即可下载并运行一个模型以 Llama 3 为例。Ollama 会自动处理模型的下载、服务封装和硬件加速配置。# 首次运行会自动下载模型之后会直接启动服务 ollama run llama3当你在终端看到模型的交互提示时即表示服务已经成功在后台启动。Ollama 默认会在http://localhost:11434/v1地址上暴露 OpenAI 兼容的 API 接口。接入HelloAgentsLLM由于 VLLM 和 Ollama 都遵循了行业标准 API将它们接入HelloAgentsLLM的过程非常简单。我们只需在实例化客户端时将它们视为一个新的provider即可。例如连接本地运行的VLLM服务llm_client HelloAgentsLLM( providervllm, modelQwen/Qwen1.5-0.5B-Chat, # 需与服务启动时指定的模型一致 base_urlhttp://localhost:8000/v1, api_keyvllm # 本地服务通常不需要真实API Key可填任意非空字符串 )或者通过设置环境变量并让客户端自动检测实现代码的零修改# 在 .env 文件中设置 LLM_BASE_URLhttp://localhost:8000/v1 LLM_API_KEYvllm # Python 代码中直接实例化即可 llm_client HelloAgentsLLM() # 将自动检测为 vllm同理连接本地的Ollama服务也一样简单llm_client HelloAgentsLLM( providerollama, modelllama3, # 需与 ollama run 指定的模型一致 base_urlhttp://localhost:11434/v1, api_keyollama # 本地服务同样不需要真实 Key )自动检测机制为了尽可能减少用户的配置负担并遵循“约定优于配置”的原则HelloAgentsLLM内部设计了两个核心辅助方法_auto_detect_provider和_resolve_credentials。它们协同工作_auto_detect_provider负责根据环境信息推断服务商而_resolve_credentials则根据推断结果完成具体的参数配置。_auto_detect_provider方法负责根据环境信息按照下述优先级顺序尝试自动推断服务商最高优先级检查特定服务商的环境变量这是最直接、最可靠的判断依据。框架会依次检查MODELSCOPE_API_KEY,OPENAI_API_KEY,ZHIPU_API_KEY等环境变量是否存在。一旦发现任何一个就会立即确定对应的服务商。次高优先级根据base_url进行判断如果用户没有设置特定服务商的密钥但设置了通用的LLM_BASE_URL框架会转而解析这个 URL。域名匹配通过检查 URL 中是否包含api-inference.modelscope.cn,api.openai.com等特征字符串来识别云服务商。端口匹配通过检查 URL 中是否包含:11434(Ollama),:8000(VLLM) 等本地服务的标准端口来识别本地部署方案。辅助判断分析 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_URLhttp://localhost:11434/v1 LLM_MODEL_IDllama3他完全不需要配置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__( contentcontent, rolerole, timestampkwargs.get(timestamp, datetime.now()), metadatakwargs.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( debugos.getenv(DEBUG, false).lower() true, log_levelos.getenv(LOG_LEVEL, INFO), temperaturefloat(os.getenv(TEMPERATURE, 0.7)), max_tokensint(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 fAgent(name{self.name}, provider{self.llm.provider})通过继承ABC被定义为一个不能直接实例化的抽象类。构造函数__init__清晰地定义了 Agent 的核心依赖名称、LLM 实例、系统提示词和配置。使用abstractmethod装饰的run方法它强制所有子类必须实现此方法从而保证了所有智能体都有统一的执行入口。此外基类还提供了通用的历史记录管理方法这些方法与Message类协同工作体现了组件间的联系。