大模型Agent开发:让AI学会使用工具与API调用

大模型Agent开发:让AI学会使用工具与API调用
在这里插入图片描述
👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕人工智能这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!

文章目录

大模型Agent开发:让AI学会使用工具与API调用 🌍🤖

自然语言处理技术的跨越式发展,正在彻底重塑我们与数字世界交互的方式。过去的大语言模型更像是一个博学的“对话者”,它擅长总结、翻译、写作与逻辑推演,但其输出始终被限制在文本生成的边界之内。然而,真正的智能并不仅仅停留在“知道”,更在于“做到”。当我们希望大模型能够实时查询天气、操作数据库、发送邮件、调用业务系统甚至完成多步跨平台的工作流时,传统的纯文本问答模式便显得捉襟见肘。于是,Agent(智能体)架构应运而生。

Agent 的核心范式可以概括为:大语言模型作为决策中枢,配合外部工具与API作为执行手脚,通过规划与记忆机制形成闭环。让AI学会使用工具与API调用,不仅是提示词工程(Prompt Engineering)的延伸,更是软件工程、系统架构与认知科学交叉融合的工程实践。本文将深入剖析Agent工具调用的底层逻辑,从工具设计、执行引擎构建、复杂工作流编排到工程化落地,提供一套可落地、可演进的技术方案 🛠️✨


🧩 Agent 的核心架构与执行闭环

在深入代码之前,我们需要先建立清晰的架构认知。一个具备工具调用能力的Agent并非黑盒,其运行遵循可预测的模块化逻辑。业界广泛认可的Agent四要素包括:

  1. 大脑(LLM Core):负责意图理解、任务拆解、逻辑推理与工具选择。
  2. 工具(Tools / APIs):外部能力的具象化,提供明确的输入输出契约。
  3. 记忆(Memory):维护上下文历史、工具调用状态与中间结果。
  4. 规划与执行(Planning & Execution):决定何时调用、按什么顺序调用、失败后如何回退。

其中最关键的交互模式是 Reason + Act(思考与行动交替)。模型不会一次性输出最终结果,而是通过“观察-思考-行动”的循环逐步逼近目标。

生成工具选择与参数

更新状态

判断是否完成

用户输入

大模型推理

路由分发

API A

API B

工具 C...

返回结构化结果

返回结构化结果

返回结构化结果

记忆与上下文

是否需要继续?

输出最终答案

上图清晰地展示了Agent的执行流。用户输入首先被模型解析,若模型判断需要外部能力,则会生成特定的工具调用指令;引擎捕获指令后,解析参数并发起真实请求;执行结果被格式化后注入上下文,模型再次进行推理,直到满足终止条件。整个过程是确定性的循环,而非随意的文本生成。


📐 工具与 API 的标准化设计 🧱

Agent能否准确调用工具,80%取决于工具定义的规范性。大语言模型本质上是概率分布的拟合器,它依赖文本描述来理解工具的边界。如果描述模糊、参数类型缺失或缺少约束条件,模型极易产生幻觉(Hallucination),例如编造不存在的参数、传入错误的数据类型,或者在应该使用GET请求时误用POST。

1. 遵循 JSON Schema 契约

现代大模型厂商普遍支持 Function Calling 或 Tool Calling 能力,其底层均依赖 JSON Schema 来描述工具签名。一个高质量的工具定义应当包含:

  • 精确的 name:语义明确,动词开头最佳(如 query_weather, create_order)。
  • 详细的 description:说明用途、适用场景、限制条件。
  • 完整的 parameters:每个字段必须标注 typedescriptionrequired,必要时使用 enum 限制取值范围。
# 工具定义示例:标准化 JSON Schema 结构 WEATHER_TOOL_SCHEMA ={"name":"get_current_weather","description":"获取指定城市的实时天气数据。仅支持中国境内主要城市。","parameters":{"type":"object","properties":{"city":{"type":"string","description":"城市名称,如北京、上海、深圳"},"unit":{"type":"string","enum":["celsius","fahrenheit"],"description":"温度单位,默认为摄氏度"}},"required":["city"]}}

在定义 description 时,务必包含模型决策所需的信息。例如,若某个API有频率限制,应在描述中注明:“此接口每日限调1000次,请优先使用缓存数据”。这能显著降低无效调用率。

2. 统一工具注册表与类型系统

在实际工程中,Agent往往需要管理几十甚至上百个工具。硬编码会导致维护灾难。推荐采用注册表模式(Registry Pattern)与类型系统结合,确保工具可发现、可验证、可调用。

from typing import Dict, Callable, Any import json classToolRegistry:def__init__(self): self._tools: Dict[str, Dict]={} self._executors: Dict[str, Callable]={}defregister(self, schema:dict, executor: Callable): name = schema["name"]if name in self._tools:raise ValueError(f"Tool '{name}' already registered.")# 验证 schema 合法性(简化版) self._validate_schema(schema) self._tools[name]= schema self._executors[name]= executor defget_tool(self, name:str):if name notin self._tools:raise KeyError(f"Unknown tool: {name}")return self._tools[name], self._executors[name]deflist_tools(self)->list:returnlist(self._tools.values())def_validate_schema(self, schema:dict): required_keys ={"name","description","parameters"}ifnot required_keys.issubset(schema.keys()):raise ValueError("Invalid tool schema: missing required keys.")

通过注册表,我们可以动态加载工具,并在运行前进行静态校验。这种设计也为后续的热更新、权限控制与审计日志打下基础 📊。


💻 从零构建 Agent 执行引擎

有了标准化工具,下一步是构建能够驱动它们的引擎。我们不依赖重型框架,而是用纯 Python 实现一个轻量、透明的执行循环,以便深入理解底层机制。

1. 核心循环与上下文管理

Agent 的运行本质是一个带状态的循环。每次迭代都需要记录:用户的原始输入、模型的历史推理、已调用的工具及其结果。上下文窗口的限制决定了我们必须合理裁剪历史,而非无限堆叠。

import json import openai from dataclasses import dataclass, field from typing import List, Optional @dataclassclassAgentContext: user_input:str conversation_history: List[dict]= field(default_factory=list) tool_calls_made: List[dict]= field(default_factory=list) max_iterations:int=5classToolAgent:def__init__(self, registry: ToolRegistry, client: openai.Client, model:str="gpt-4o"): self.registry = registry self.client = client self.model = model defrun(self, context: AgentContext)->str: messages =[{"role":"system","content": self._build_system_prompt()},{"role":"user","content": context.user_input}]+ context.conversation_history for _ inrange(context.max_iterations):# 1. 调用 LLM 获取决策 response = self.client.chat.completions.create( model=self.model, messages=messages, tools=self.registry.list_tools(), tool_choice="auto") choice = response.choices[0].message # 2. 若没有工具调用,直接返回最终答案ifnot choice.tool_calls:return choice.content # 3. 处理工具调用 tool_results =[]for tc in choice.tool_calls: tool_name = tc.function.name arguments = json.loads(tc.function.arguments)try:# 获取真实执行器并调用 _, executor = self.registry.get_tool(tool_name) result = executor(**arguments) tool_results.append({"tool_call_id": tc.id,"tool_name": tool_name,"output": json.dumps(result, ensure_ascii=False)}) context.tool_calls_made.append({"name": tool_name,"args": arguments,"result": result })except Exception as e: tool_results.append({"tool_call_id": tc.id,"tool_name": tool_name,"output":f"Error: {str(e)}"})# 4. 将模型的工具调用与执行结果回传 messages.append(choice)for tr in tool_results: messages.append({"role":"tool","tool_call_id": tr["tool_call_id"],"content": tr["output"]})return"Agent reached maximum iterations without completing the task."def_build_system_prompt(self)->str:return"""你是一个智能助手。你可以使用已注册的工具来完成任务。 请严格遵循以下原则: 1. 只有在必要时才调用工具。 2. 工具参数必须符合 JSON Schema 定义,禁止编造字段。 3. 根据工具返回的结果继续推理,直到任务完成。 4. 若遇到错误,请分析原因并尝试调整参数重试,或告知用户具体限制。"""

上述引擎实现了标准的 OpenAI Function Calling 协议。其优势在于:逻辑透明、易于调试、与底层协议直接对齐。在实际生产环境中,你可以替换 client 为任何兼容 OpenAI 格式的 API(包括本地部署的 vLLM、Ollama 或商业云服务)。

2. 真实 API 调用封装

工具的执行函数(Executor)需要处理网络请求、认证、重试与异常。以封装一个第三方汇率查询 API 为例:

import requests from functools import lru_cache # 配置常量 API_BASE_URL ="https://open.er-api.com/v6/latest" DEFAULT_TIMEOUT =10defget_exchange_rate(base_currency:str, target_currency:str, date: Optional[str]=None)->dict:"""查询实时汇率并返回标准化结果"""ifnot base_currency.isalpha()ornot target_currency.isalpha():raise ValueError("Currency codes must be alphabetic (e.g., USD, CNY).") endpoint =f"{API_BASE_URL}/{base_currency}" params ={"timeout": DEFAULT_TIMEOUT}if date: params["date"]= date try: response = requests.get(endpoint, params=params) response.raise_for_status() data = response.json()if"rates"notin data or target_currency notin data["rates"]:raise KeyError(f"Target currency {target_currency} not supported.")return{"base": base_currency,"target": target_currency,"rate": data["rates"][target_currency],"timestamp": data.get("time_last_updated"),"source":"er-api.com"}except requests.exceptions.Timeout:raise RuntimeError("汇率查询超时,请稍后重试。")except requests.exceptions.RequestException as e:raise RuntimeError(f"网络请求失败: {str(e)}")# 缓存装饰器:减少重复调用与API压力 cached_rate = lru_cache(maxsize=100)(get_exchange_rate)

通过 lru_cache,我们可以对高频查询进行内存缓存;通过结构化异常抛出,Agent 能明确知道失败原因并决定下一步策略。这是工程实践中极易被忽视的细节 🔧。


🔄 复杂工作流与状态编排

真实业务场景中,任务往往不是单步调用能解决的。例如:“查询北京天气,如果下雨则查询地铁延误信息,并帮我起草一份延期邮件”。这涉及条件分支、多工具串联与结果聚合。

1. 动态规划 vs 预设工作流

Agent 处理复杂任务有两种主流思路:

  • LLM 动态规划:模型在每一步决定下一个工具。灵活性强,但可能陷入循环或偏离目标。
  • 图驱动工作流(DAG):预先定义节点与边,Agent 负责填参或路由。稳定性高,但灵活性受限。

对于金融、政务等高可靠场景,推荐混合模式:用图定义主干流程,允许 Agent 在子节点内自主选择辅助工具。

用户输入

识别意图与实体

调用主工具

检查结果状态

条件: 失败/部分数据

条件: 成功

自动重试策略

成功恢复

失败上限

整合数据

格式化/翻译

IntentParse

ToolSelection

ExecutePrimary

CheckCondition

ExecuteFallback

SynthesizeResult

RetryOrAbort

ErrorNotify

GenerateOutput

FormatResponse

该状态图展示了健壮的异常恢复机制。关键在于将“条件判断”与“重试逻辑”工程化,而非完全依赖模型自我纠错。

2. 多步状态传递示例

如何在引擎中维护跨步骤的状态?推荐使用显式的 State 对象,而非隐式拼接字符串。

from dataclasses import dataclass, field from typing import Any, Dict, List @dataclassclassTaskState: goal:str current_step:int=0 variables: Dict[str, Any]= field(default_factory=dict) history: List[Dict]= field(default_factory=list)defset_var(self, key:str, value: Any): self.variables[key]= value self.history.append({"step": self.current_step,"action":"set_var","data":{key: value}})defget_var(self, key:str, default=None):return self.variables.get(key, default)defnext_step(self): self.current_step +=1classWorkflowOrchestrator:def__init__(self, agent: ToolAgent): self.agent = agent defrun_weather_trip_plan(self, state: TaskState)->str:# Step 1: 获取天气 city = state.get_var("city","北京") weather_tool, exec_func = agent.registry.get_tool("get_current_weather") weather_data = exec_func(city=city) state.set_var("weather_condition", weather_data.get("condition","unknown")) state.set_var("temperature", weather_data.get("temperature"))# Step 2: 条件分支 condition = state.get_var("weather_condition").lower()if"rain"in condition: state.next_step()print("🌧️ 检测到雨天,触发备用路线查询...") transport_tool, transport_exec = agent.registry.get_tool("check_subway_delay") delay_info = transport_exec(city=city) state.set_var("delay_notice", delay_info)# Step 3: 生成最终回复 state.next_step() prompt =f"""基于以下数据生成行程建议: 城市: {city} 天气: {state.get_var('weather_condition')},气温: {state.get_var('temperature')}{'地铁延迟提示: '+str(state.get_var('delay_notice'))if state.get_var('delay_notice')else'无延迟'} """return agent.run(AgentContext(user_input=prompt, max_iterations=3))

通过显式状态对象,调试器可以精确打印每一步的变量快照,大幅降低“模型乱调参数”的排查成本。在分布式环境中,该 State 还可直接序列化存入 Redis 或数据库,实现断点续跑 ⚡。


🛡️ 工程化实践:稳定性、安全与可观测性

当 Agent 从实验走向生产,纯算法层面的优化已不足以支撑系统可靠性。我们必须引入软件工程的标准防线。

1. 限流、超时与熔断

外部 API 并非永远可用。网络抖动、第三方服务降级、密钥过期都是常态。Agent 引擎必须内置弹性策略:

  • 指数退避重试:避免在 API 故障时发起雪崩请求。
  • 请求超时控制:单个工具调用不应阻塞整个 Agent 循环。建议设置硬性上限(如 15 秒)。
  • 熔断器模式:当连续失败超过阈值,暂时禁用该工具,降级为提示用户或走本地缓存逻辑。
import time from functools import wraps from requests.exceptions import RequestException defresilient_call(max_retries:int=3, base_delay:float=1.0):defdecorator(func):@wraps(func)defwrapper(*args,**kwargs):for attempt inrange(max_retries):try:return func(*args,**kwargs)except(RequestException, TimeoutError)as e:if attempt == max_retries -1:raise delay = base_delay *(2** attempt)print(f"⏳ 工具调用失败 (Attempt {attempt+1}), {delay:.1f}s 后重试... 原因: {e}") time.sleep(delay)except Exception as e:# 业务逻辑错误或参数错误,不重试raise RuntimeError(f"Critical error: {e}")returnNonereturn wrapper return decorator @resilient_call(max_retries=2, base_delay=0.5)defexternal_api_call(endpoint:str, payload:dict):# 模拟可能不稳定的外部调用pass

2. 安全沙箱与权限隔离

赋予 AI 调用 API 的权限,等同于赋予其执行系统指令的能力。安全设计必须前置:

  • 最小权限原则:每个工具使用独立的、权限受限的 API Key,而非共享超级凭证。
  • 输入消毒:对用户传入或模型生成的参数进行白名单校验,防止 SQL 注入、路径穿越或恶意代码执行。
  • 输出脱敏:工具返回结果中可能包含敏感信息(如内部 ID、手机号),应在注入上下文前进行过滤或哈希处理。
  • 人工审批环(Human-in-the-loop):对于写操作(如删除、支付、修改配置),Agent 应生成操作摘要并要求用户确认,而非直接执行。

可参考 OpenAI 的官方安全指南来构建权限模型:https://platform.openai.com/docs/guides/safety-best-practices

3. 可观测性(Observability)

当 Agent 表现异常时,你需要回答三个问题:模型思考了什么?调用了什么工具?为什么选这个工具而非那个?日志系统必须记录完整的 Trace:

import logging from datetime import datetime classAgentLogger:def__init__(self, run_id:str): self.run_id = run_id self.logger = logging.getLogger(f"Agent.{run_id}") self.logger.setLevel(logging.DEBUG)# 实际项目应接入 OpenTelemetry、LangSmith 或 Weave 等链路追踪平台# 参考文档: https://www.langchain.com/langsmith# 参考文档: https://www.wandb.ai/site/weave handler = logging.StreamHandler() formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") handler.setFormatter(formatter) self.logger.addHandler(handler)deflog_decision(self, thought:str, tool_name:str, params:dict): self.logger.info(f"🧠 THOUGHT: {thought}") self.logger.info(f"🛠️ ACTION: {tool_name} | Params: {json.dumps(params, ensure_ascii=False)}")deflog_observation(self, tool_name:str, output:str): self.logger.info(f"👁️ OBSERVATION ({tool_name}): {output}")deflog_finish(self, answer:str, steps:int): self.logger.info(f"✅ FINISH | Steps: {steps} | Answer: {answer[:100]}...")

结构化日志不仅是调试利器,更是模型迭代的数据源。通过分析高频失败的工具调用路径,你可以优化工具描述、调整温度参数,甚至拆分职责过重的复合工具 🔍。


🚀 落地指南与常见陷阱

在将 Agent 推向生产环境前,请务必关注以下实战经验:

  1. 不要过度信任模型的推理能力:即使是最强的模型,在复杂数学计算或精确格式转换上也会出错。该用代码解决的(如正则提取、时间计算),不要强求模型输出纯文本结果。
  2. 工具描述即代码description 的权重不亚于函数签名。定期收集用户 Query 与工具调用命中率,使用数据驱动的方式重写描述。
  3. 控制上下文膨胀:每次工具调用返回的结果可能长达数千 token。使用摘要器(Summarizer)压缩冗余数据,或采用滑动窗口截取关键片段。
  4. 冷启动与兜底策略:新上线的工具或冷门的 API 可能缺乏模型训练数据。提供少量 Few-Shot 示例(在 system prompt 中展示正确调用范例),能显著提升首次成功率。
  5. 评估体系先行:不要等到上线才发现 Agent 在乱调用。构建自动化评估集:输入标准 Query,断言期望调用的工具名、参数及最终输出格式。参考 JSON Schema 验证标准:https://json-schema.org/understanding-json-schema/index

🌅 结语:从“对话者”到“协作者”的进化

大模型 Agent 的开发,正在经历从“魔法调用”到“系统工程”的范式转移。工具调用不再是简单的 API 转发,而是意图理解、契约验证、状态管理、容错恢复与可观测性的综合体。当我们让 AI 学会使用工具,我们实际上是在构建一种全新的交互协议:人类定义目标与边界,机器负责路径规划与精准执行。

未来的 Agent 将不再局限于单轮对话与预设工具。它们将具备自主发现新工具的能力(通过阅读文档生成 Schema)、支持多智能体协作(分工、竞合、共识)、并在严格的安全护栏内实现更高程度的自动化。但无论架构如何演进,核心原则始终不变:清晰的契约、透明的状态、健壮的容错、可审计的轨迹。

技术之路没有捷径,但每一次参数调优、每一次异常捕获、每一次对 Prompt 的打磨,都在让这个数字协作者变得更加可靠。拿起键盘,定义你的第一个工具,观察它在模型中激起的第一次推理涟漪吧。当你看到 Agent 精准地串联起多个 API,最终交付一份超出预期的结果时,你会真切地感受到:智能,正在从云端走向指尖 🌐✨


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Read more

【FPGA入坑指南第二章】安装vivado/vitis2023.1软件

【FPGA入坑指南第二章】安装vivado/vitis2023.1软件

本栏目的初心 降低FPGA的门槛,让所有对FPGA感兴趣的,之前望而却步的朋友也能上手玩一玩,体验一下FPGA的世界。【本栏作者贯彻“先进入再深入”的中心思想】 引文 * AMD官方软件下载地址 vivado开发者工具 * 百度云下载包 Xilinx2023.1安装包「其他版本可以联系作者」 简介 Vivado和Vitis是Xilinx(现为AMD的一部分)推出的两款核心软件工具,它们在FPGA和SoC(系统级芯片)设计中占据着重要地位。这两款软件的推出代表了Xilinx在数字设计领域的持续创新与发展,并且逐步取代了早期的ISE和SDK工具套件。 ISE和SDK的历史背景 在Vivado和Vitis推出之前,Xilinx的ISE(Integrated Software Environment)是FPGA设计的主要开发环境。ISE主要用于Xilinx早期的FPGA系列,如Spartan和Virtex系列。ISE支持从RTL设计、综合、布局布线到生成比特流文件的整个设计流程,但其在时序优化、设计复杂度和开发效率方面逐渐暴露出一些局限性,尤其是对于更高端的FPGA系列和

无人机数据集汇总无人机航拍各个方面检测分割数据集合集

本数据集集合了面向无人机视觉任务的大规模、多场景、多目标标注数据资源,涵盖了地理环境、智慧城市、基础设施巡检、农业生产、公共安全与灾害监测等多个关键领域。数据主要以两种主流格式提供:适用于目标检测的VOC/YOLO格式与适用于像素级语义分割的LabelMe格式,为算法开发与模型训练提供了高度结构化的标注支持。 在地理与农业监测方面,包含田地、道路、森林、水体等地理要素的分割数据集,以及作物病害、杂草识别、农田农机、牛羊牲畜等农业目标的检测数据,支持精准农业与生态研究。智慧城市与交通领域提供了丰富的城市街道场景数据,涵盖行人、车辆、交通标志、占道经营、消防通道、广告牌等目标的检测与分割,助力城市智能化管理。基础设施巡检是另一重点,覆盖电力线、光伏板、桥梁、铁路、风力发电机等设备的缺陷与异常检测,以及工地车辆、施工人员、物料垃圾的识别,满足工业自动化巡检需求。在灾害与安全监控中,包含滑坡、洪水、火灾烟雾、河道垃圾、违规建筑等应急场景的检测与分割数据,同时提供了溺水人员、海上救援、军事目标等特殊任务的专项数据集。此外,

Project IceStorm:开源FPGA比特流逆向工程与开发工具套件

Project IceStorm:开源FPGA比特流逆向工程与开发工具套件 【免费下载链接】icestorm 项目地址: https://gitcode.com/gh_mirrors/ice/icestorm Project IceStorm是一个专注于Lattice iCE40 FPGA系列的开源逆向工程项目,通过深入解析比特流格式,为硬件开发者提供完整的FPGA开发工具链。该项目彻底改变了传统FPGA开发依赖商业工具链的局面,让开发者能够更深入地理解和控制FPGA的底层配置。 🔧 核心工具解析 比特流处理工具 icepack - 位图文件打包工具 位于 icepack/ 目录,负责将逻辑网表转换为FPGA可识别的比特流格式,实现设计到硬件的最终转换。 iceunpack - 比特流解包分析 能够逆向解析现有的比特流文件,提取其中的配置信息,帮助开发者理解FPGA内部资源的使用情况。 时序分析与优化 icetime - 时序分析与路径优化 位于 icetime/ 目录,提供详细的时序报告和路径延迟分析,确保设计满足时序约束要求。 硬件编程接口 iceprog

【无人机3D路径规划】基于改进蝙蝠优化算法的无人机3D路径规划研究附Matlab代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、建模仿真、程序设计、完整代码获取、论文复现及科研仿真。 🍎 往期回顾关注个人主页:Matlab科研工作室  👇 关注我领取海量matlab电子书和数学建模资料  🍊个人信条:格物致知,完整Matlab代码获取及仿真咨询内容私信。 🔥 内容介绍  一、引言 在当今科技飞速发展的时代,无人机在众多领域得到了广泛应用,从物流配送、农业监测到航空测绘等。在这些应用场景中,无人机需要在三维空间中规划出一条安全、高效的飞行路径,以完成各种任务。传统的路径规划算法在处理复杂的 3D 环境时,往往存在收敛速度慢、易陷入局部最优等问题。蝙蝠优化算法(Bat Algorithm,BA)作为一种新兴的智能优化算法,模拟了蝙蝠的回声定位行为,为解决此类问题提供了新的思路。然而,标准的蝙蝠优化算法也有其局限性,因此本文聚焦于基于改进蝙蝠优化算法的无人机 3D 路径规划研究,旨在提升路径规划的性能。 二、蝙蝠优化算法基础 1. 蝙蝠回声定位模拟:蝙蝠在飞行过程中通过发出超声波,并根据回声来感知周围环