AI Agent实战:从LLM到自定义智能体的完整开发指南
2026/9/7 14:06:31 网站建设 项目流程

1. AI Agent 到底是什么:从 LLM 到 Agent 的进化

这两年,AI 领域最热的关键词已经从“大模型”本身,慢慢转移到了“AI Agent”。很多人第一次听到这个概念时,会误以为 AI Agent 就是“接入了大模型的聊天机器人”,或者“能自动回复消息的智能客服”。实际上,这两者虽然有重叠,但差异非常大。

如果用一个通俗的比喻来理解:

  • 大模型(LLM)本身像一个知识渊博但“呆在书房里”的顾问,你问它问题,它基于训练数据给你答案。它能写文章、写代码、做翻译,但它无法替你执行任何现实世界中的操作。
  • AI Agent 则像是一个“有手有脚、有目标感”的智能助理。它不只是回答你的问题,而是把任务拆解成步骤,主动调用工具、查询数据、执行操作,并在过程中根据反馈调整策略,直到完成一个最终目标。

举个例子:你让大模型“帮我查一下明天北京的天气,并提醒我是否需要带伞”,普通的大模型只能告诉你“我无法实时查询天气”。而一个 AI Agent 会先调用天气查询接口,拿到明天下雨的概率,再结合“下雨需要带伞”的规则,最后输出一条完整的提醒。整个过程需要模型、工具、决策逻辑、结果验证四部分协同工作。

从专业定义来看,AI Agent 是“以大模型为推理大脑,通过规划(Planning)、记忆(Memory)、工具调用(Tool Use)和行动(Action)四要素,完成特定目标任务的智能体系统”。它不仅是调用 API,而是把大模型嵌入到一个完整的工作循环中。

理解这个区别,是进入 Agent 开发的第一步。下面我们会从环境搭建开始,逐步拆解 AI Agent 的底层原理,然后手把手实现一个自定义智能体。

2. 开发环境准备:搭建第一套 Agent 工程

2.1 环境依赖清单

开发 AI Agent 本质上还是写代码,所以一套干净、稳定的 Python 开发环境是必须的。本文的实战示例将以 Python 为主要语言,核心依赖如下:

依赖库用途说明
openai调用 LLM 接口,支持 OpenAI 官方接口和兼容性 API
langchain简化 Agent、工具、记忆的编排逻辑(可选,但推荐)
python-dotenv管理 API Key 等环境变量,避免硬编码
fastapi将 Agent 包装成 Web 服务(进阶可选)
httpx / requests自定义工具函数中发起 HTTP 请求

版本方面,本文示例以 Python 3.9 及以上版本为基准。实际上,不同大模型 SDK 的版本差异较大,建议读者根据自己实际使用的模型服务商调整依赖版本,重点理解整体开发流程。

需要特别说明的是,本文核心代码思路不绑定单一厂商。如果你使用的是国内大模型平台的 OpenAI 兼容接口,只需要修改 base_url 和 api_key 即可复用。

2.2 创建项目结构

建议先按下面的目录结构组织工程,后续所有代码示例都基于这个结构展开:

ai-agent-tutorial/ ├── .env # 存放 API Key 等敏感信息 ├── requirements.txt # 项目依赖清单 ├── agent/ │ ├── __init__.py │ ├── llm.py # 大模型调用封装 │ ├── tools.py # 工具函数定义 │ ├── memory.py # 记忆管理(暂用简单列表) │ └── agent.py # Agent 核心逻辑 ├── main.py # 入口文件,演示交互循环 └── README.md

这样拆分的好处是:模型接入、工具注册、记忆逻辑、流程控制各自独立,后续想替换模型或新增工具时,不需要大面积改动代码。

2.3 安装依赖

首先创建虚拟环境并激活:

cd ai-agent-tutorial python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate

然后创建requirements.txt并安装依赖:

openai>=1.0.0 langchain>=0.1.0 python-dotenv>=1.0.0 httpx>=0.24.0

执行安装命令:

pip install -r requirements.txt

以上版本号只是示例区间,真实的版本兼容情况请以你的 Python 版本和模型服务商 SDK 文档为准。核心思路是尽量使用较新的 SDK,因为早期版本在 Function Calling 和异步支持方面可能有差异。

3. AI Agent 核心原理拆解

在动手写代码之前,我建议先把 Agent 的四个核心模块理解透。很多人照着教程能跑通 Demo,但一旦要解决线上问题就束手无策,根本原因是原理没搞懂。

3.1 LLM 与 Agent 的分工关系

简单说,LLM 是 Agent 的“大脑”,负责理解、推理和生成决策。但 LLM 本身没有“执行力”。Agent 的作用是在 LLM 外面包一层“执行循环”:

  1. 接收用户目标。
  2. 把目标交给 LLM,让模型判断需要调用什么工具、需要什么参数。
  3. 代码侧执行工具函数,拿到真实结果。
  4. 把结果返回给 LLM,让模型判断任务是否完成。
  5. 如果未完成,继续循环;如果完成,生成最终答案。

这个循环就是我们常说的 ReAct(Reasoning + Acting)模式,即“推理-行动-观察”交替进行。ReAct 的核心价值在于:它让模型不再只是“一次性输出答案”,而是“边想边做,做完再看,看完再想”。

3.2 工具调用(Function Calling)机制

工具调用是 Agent 区别于普通聊天机器人的关键能力。大模型本身无法查天气、没法操作数据库、没法调用内部 API,但我们可以通过“Function Calling”把外部能力暴露给模型。

原理是这样的:

  • 开发者在请求中声明一批 JSON 格式的工具描述,包括函数名、参数说明、功能描述。
  • 模型的输出不再直接是最终答案,而是“要不要调用某个函数、参数是什么”。
  • 代码侧负责真正执行这个函数,并把执行结果作为“观察”喂回模型。

这种设计最大的优势是安全可控。真正执行代码的是我们自己,模型只负责“决定调用哪个函数、传什么参数”,不会直接操作系统。

3.3 记忆机制:短期与长期

Agent 要解决复杂任务,离不开记忆。简单场景下,记忆就是一个历史消息列表,把之前的对话轮流塞进上下文,让模型知道“前面发生过什么”。这种方法叫做短期记忆,实现最简单,但受限于模型的上下文窗口长度。

当对话历史太长,超出上下文窗口时,就需要引入长期记忆方案。常见思路包括:

  • 用向量数据库存储历史消息,按相关性检索摘要后注入上下文。
  • 定期对历史记录做摘要压缩,只保留关键信息。
  • 把结构化信息(如用户偏好、任务状态)单独存储,用的时候再读取。

在本文的实战部分,我们先用列表实现短期记忆。工程化落地时,再考虑接向量数据库。

3.4 规划与任务分解

一个真正好用的 Agent,必须能把大任务拆成小步骤。比如用户说“帮我调研一下 RAG 技术的发展趋势,并输出一份报告”,Agent 不能一次性生成一份高质量报告,而是应该拆解为:

  1. 搜索相关资料。
  2. 整理核心观点。
  3. 撰写报告大纲。
  4. 分层填充报告内容。
  5. 校验和输出。

这个拆解过程可以由 LLM 自主完成,也可以由开发者在 Prompt 中预定义“标准作业流程”。实际项目中,我建议采用“预定义流程 + 模型动态调整”的混合模式,这样稳定性更高,不会让模型完全自由发挥导致跑偏。

4. 从零实现自定义智能体:完整实战案例

下面进入本文的核心环节:写一个可运行的自定义智能体。我们会实现一个支持天气查询和简单计算功能的 Agent,代码保持精简,但流程完整,读者可以直接在此基础上扩展。

4.1 配置环境变量

在项目根目录创建.env文件:

# 模型服务商 API Key OPENAI_API_KEY=your_api_key_here # 如果使用 OpenAI 兼容接口,修改为对应地址 OPENAI_BASE_URL=https://api.openai.com/v1 # 模型名称 LLM_MODEL=gpt-3.5-turbo

注意:不同服务商的 API Key 获取方式不一样,请根据你自己的账号体系配置。实际项目中不要把 Key 提交到 Git 仓库,.env必须加入.gitignore

4.2 封装 LLM 调用模块

文件路径:agent/llm.py

import os from openai import OpenAI from dotenv import load_dotenv load_dotenv() client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), ) def chat_with_llm(messages, tools=None, tool_choice="auto"): """ 统一的 LLM 调用入口。 参数说明: - messages: 历史消息列表,格式为 OpenAI Chat Completions 标准格式 - tools: 工具描述列表,每个工具包含 type、function 等字段 - tool_choice: 控制模型是否必须调用工具,默认 auto 表示由模型自行判断 """ params = { "model": os.getenv("LLM_MODEL", "gpt-3.5-turbo"), "messages": messages, } if tools: params["tools"] = tools params["tool_choice"] = tool_choice response = client.chat.completions.create(**params) return response.choices[0].message

这个模块的核心价值是把模型调用统一封装。后续无论想增加重试机制、日志记录,还是切换不同模型,都只需改这一个文件。

4.3 定义工具函数

文件路径:agent/tools.py

import json import httpx def get_weather(city: str) -> str: """ 查询城市天气。这里为演示目的,使用一个公开的天气 API。 注意:实际项目中请替换为你自己的天气服务地址。 """ url = f"https://api.open-meteo.com/v1/forecast?latitude=39.9&longitude=116.4&current_weather=true" # 真实场景中,应该根据 city 查询经纬度。这里简化为调用固定城市。 try: resp = httpx.get(url, timeout=10) data = resp.json() temp = data["current_weather"]["temperature"] wind_speed = data["current_weather"]["windspeed"] return json.dumps({"city": city, "temperature": temp, "wind_speed": wind_speed}, ensure_ascii=False) except Exception as e: return json.dumps({"error": str(e)}, ensure_ascii=False) def calculator(expression: str) -> str: """ 计算数学表达式。注意:这里使用 eval 仅作为教学示例, 生产环境中必须使用安全的表达式计算库(如 asteval)。 """ # 安全校验:只允许数字、运算符、括号、小数点 import re if not re.fullmatch(r"[\d+\-*/().\s]+", expression): return "非法表达式" try: result = eval(expression) # 生产环境请替换为安全方案 return str(result) except Exception as e: return f"计算错误: {e}" # 工具注册表:Agent 通过这个字典找到对应的执行函数 TOOL_FUNCTIONS = { "get_weather": get_weather, "calculator": calculator, } def get_tool_schemas(): """ 返回大模型可识别的工具 JSON Schema 列表。 """ return [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市的实时天气,包括温度和风速", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,如 北京、上海", } }, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "calculator", "description": "计算数学表达式,如 1 + 2 * 3", "parameters": { "type": "object", "properties": { "expression": { "type": "string", "description": "合法的数学表达式", } }, "required": ["expression"], }, }, }, ]

这里需要特别说明一点:工具函数的描述写得好不好,直接影响模型判断的准确率。在 Function Calling 机制中,模型是通过函数名和描述来理解“这个工具能干什么”的。描述写得模糊,模型就会在多个工具之间犹豫,甚至返回错误的参数。所以,工具描述本身就是一种 Prompt Engineering

4.4 实现 Agent 核心循环

文件路径:agent/agent.py

from .llm import chat_with_llm from .tools import get_tool_schemas, TOOL_FUNCTIONS import json class SimpleAgent: """ 一个最简单的 ReAct 模式 Agent。 它只做三件事: 1. 调用大模型判断下一步动作。 2. 如果需要工具,就执行工具并回传结果。 3. 如果不需要工具,就输出最终回答。 """ def __init__(self, system_prompt: str = None): self.messages = [] if system_prompt: self.messages.append({"role": "system", "content": system_prompt}) self.tools = get_tool_schemas() def run(self, user_input: str, max_steps: int = 5) -> str: """ 运行 Agent。 参数说明: - user_input: 用户输入 - max_steps: 最大循环步数,防止 Agent 陷入死循环 """ # 1. 添加用户消息 self.messages.append({"role": "user", "content": user_input}) current_step = 0 while current_step < max_steps: current_step += 1 print(f"[Step {current_step}] 调用大模型判断下一步...") # 2. 调用大模型 assistant_message = chat_with_llm(self.messages, tools=self.tools) # 3. 模型判断是否需要调用工具 if assistant_message.tool_calls: # 4. 执行工具调用 self.messages.append(assistant_message) for tool_call in assistant_message.tool_calls: function_name = tool_call.function.name function_args = json.loads(tool_call.function.arguments) print(f"[Tool] 调用 {function_name}, 参数: {function_args}") # 从注册表获取并执行函数 if function_name in TOOL_FUNCTIONS: function_result = TOOL_FUNCTIONS[function_name](**function_args) else: function_result = f"未知工具: {function_name}" # 5. 把工具结果以 tool 角色消息回传 self.messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": function_result, }) # 继续循环,让模型基于工具结果做下一步判断 continue else: # 没有工具调用,说明模型认为任务已完成 final_answer = assistant_message.content self.messages.append(assistant_message) return final_answer return "已达到最大步数,任务停止。请尝试把任务拆得更细一些。"

这个 Agent 的核心逻辑就是前面说的 ReAct 循环。代码里有两个非常关键的设计:

第一,max_steps的限制。真实的 Agent 开发中,模型可能出现“反复调用同一个工具却得不到正确结果”的死循环。如果不加步数上限,程序会一直打调用,浪费 token 和时间。因此,给循环加一个硬性上限是工程底线。

第二,工具结果的回传格式。OpenAI 的 Function Calling 要求工具结果必须包含tool_call_id,并与模型的调用请求一一对应。如果你发现自己实现的 Agent“调用工具后,模型理解不了结果”,优先检查这个 ID 是否匹配。

4.5 编写主程序

文件路径:main.py

from agent.agent import SimpleAgent # 给 Agent 一个系统提示词,定义它的行为方式 SYSTEM_PROMPT = """ 你是一个智能助手,可以通过工具获取实时信息并回答问题。 当你需要查询天气时,调用 get_weather 工具。 当你需要计算数学表达式时,调用 calculator 工具。 如果工具结果不足以回答用户问题,请继续调用工具,直到获得足够信息。 回答时使用中文,语言友好、简洁、准确。 """ def main(): agent = SimpleAgent(system_prompt=SYSTEM_PROMPT) print("AI Agent 已启动。输入 'exit' 结束对话。") while True: user_input = input("\n你: ") if user_input.lower() == "exit": print("再见!") break try: response = agent.run(user_input) print(f"\nAgent: {response}") except Exception as e: print(f"\nAgent 出错: {e}") if __name__ == "__main__": main()

这一步把前面所有模块串了起来。用户输入一句话,Agent 决定是直接回答、查天气、还是先算一个表达式的值。

4.6 运行与验证

在项目根目录执行:

python main.py

尝试以下输入:

你: 北京今天天气怎么样?

预期流程:

  1. 模型判断需要查询北京天气。
  2. 调用get_weather工具。
  3. 拿到气温和风速后,组织成自然语言回答。

再试一个组合问题:

你: 帮我计算 (12 + 34) * 5 的结果,另外,上海今天冷吗?

预期流程:

  1. 模型判断需要调用calculator计算表达式。
  2. 模型判断需要调用get_weather查询上海天气。
  3. 模型综合两个工具的结果,生成完整回答。

注意,一次循环中模型可以同时发起多个工具调用,我们的代码用的是for tool_call in assistant_message.tool_calls来逐个处理,这比逐个串行询问模型更高效。

5. 常见报错与排查清单

Agent 开发中坑点不少,很多问题单独看文档很难定位。下面整理了一份高频问题排查清单,基本覆盖了新手阶段的大部分困惑。

问题现象常见原因解决思路
模型返回“无法调用工具”或忽略工具工具 schema 格式错误,或描述不清晰检查 tools 参数是否为标准 JSON Schema 格式;完善工具的 description,明确触发场景
调用工具时报错:provider rejected the request schema or tool payload工具参数格式与模型要求不一致,或 SDK 版本过旧更新 openai SDK 到 1.0 以上;用官方 API 调试工具打印完整的 tools 结构
请求超时,提示 model did not produce a response网络波动、模型推理时间过长、系统提示词引导模型出现死循环对请求设置超时与重试机制;精简上下文;检查是否因为工具无限循环导致单轮推理过长
模型乱传参数,工具执行失败工具参数缺少类型校验和默认值在函数入口增加参数校验;把参数描述写得更具体,例如“城市名称必须是中文全称”
Agent 反复调用同一个工具但无法结束缺少任务完成条件或 max_steps 限制在系统提示词中明确“当获取足够信息后输出最终答案”;代码中必须限制最大循环次数
上下文越来越长,导致 token 超限消息历史一直累积,没有截断或摘要实现记忆管理:删除最旧消息、做摘要压缩、或把历史写入向量数据库
API Key 泄露到代码仓库直接把 Key 写在代码里,忘记使用环境变量使用 .env 文件管理敏感信息;加入 .gitignore;必要时轮换 Key
不同模型对工具调用的格式支持不一致切换模型后没有检查新模型的 Function Calling 兼容性先查阅目标模型的官方文档,确认工具调用格式是否与 OpenAI 兼容

6. 工程化最佳实践与安全边界

上面的 Demo 跑通之后,距离生产级 Agent 还有距离。下面这些经验是实际项目中最常踩的坑,也是面试时的高频考点。

6.1 Prompt 与工具描述的持续调优

Agent 的效果好坏,很大程度上取决于两个文本:系统提示词和工具描述。它们共同决定了模型的“行为边界”。每次修改工具后,都应该重新测试旧的用例,确保没有回归。建议把测试用例沉淀成自动化回归集,每次改动后统一跑一遍。

具体来说:

  • 系统提示词要明确“何时该调用工具、何时直接回答”。
  • 工具描述要写清“触发条件、参数含义、返回结果格式”。
  • 对核心场景,可以在提示词中给出“标准输出格式”,减少模型自由发挥的空间。

6.2 安全边界:工具执行必须可控

Agent 能调用工具,也就意味着攻击面扩大。日常开发中,以下几个安全原则一定要遵守:

  • 最小权限原则:Agent 只能访问完成当前任务所必需的资源,不要给它万能的管理员权限。
  • 输入校验:所有传给工具函数的参数,必须经过合法性校验。例如本文的calculator工具,直接用eval只是教学演示,生产环境必须使用asteval这类安全的表达式解析库。
  • 操作确认:涉及删除、修改、资金操作等高风险动作,必须有二次确认机制,不能让 Agent 自动执行。
  • 日志留存:每一次工具调用都要记录完整的输入与输出,方便事后审计。

6.3 可观测性与调试

Agent 应用最大的难题是“不可控”。你很难定位一个错误回答到底是模型理解错了、工具数据错了、还是 Prompt 引导不对。因此,可观测性设计必须从一开始就建立:

  • 打印或记录每一步的完整请求与响应,特别是 tool_calls 的内容。
  • 统计每一次调用的 token 消耗、耗时,便于成本控制。
  • 为每次用户会话生成唯一的 trace_id,串联整个 ReAct 循环。

6.4 成本控制与性能优化

Agent 比普通聊天多出多轮模型调用,token 成本呈倍数增加。优化方向主要有:

  • 缩短工具返回内容:如果工具返回很长,可以只提取关键字段回传。
  • 合并工具:把多个功能合并成一个“综合查询”工具,减少模型判断次数。
  • 缓存:对高频、结果变化不大的查询(如常见城市的天气),做短时缓存。
  • 模型分级:简单意图用便宜的小模型,复杂推理才调用大模型。

6.5 测试策略

Agent 测试不同于传统软件测试,重点在于“输出是否符合预期”,而不仅仅是“程序是否报错”。常用手段包括:

  • 单元测试:单独测试每个工具函数,确保输入输出正确。
  • 场景测试:构造典型用户问题,验证 Agent 是否能正确选择工具并完成任务。
  • 防御性测试:给 Agent 发越权指令、模糊输入、恶意代码片段,验证安全机制是否生效。
  • 回归测试:修改 Prompt 或工具后,跑一遍历史用例,防止效果下降。

7. 学习路线与进阶方向

到这一步,你已经从零实现了一个具备工具调用和基础记忆的 Agent。接下来的进阶路线,建议按下面的层次逐步深入。

第一层:深入理解模型能力边界。熟练使用 Function Calling、JSON Mode、流式输出等基础能力,理解不同模型在这些能力上的差异。多动手测试,不要只看文档。

第二层:学习主流 Agent 框架。LangChain、LlamaIndex、Dify 等框架都封装了大量 Agent 基础能力。但建议先理解本文实现的手写循环,再上手框架,这样才能在框架出问题时知道底层是怎么回事。

第三层:掌握记忆与知识增强。把短期记忆升级为向量数据库 + 长期记忆,结合 RAG 技术让 Agent 能访问企业私有知识库。这一块在真实项目中需求量最大,尤其是知识库问答型 Agent。

第四层:多 Agent 协作。把一个大 Agent 拆成多个专职 Agent,由调度 Agent 分配任务。这能解决单个 Agent 上下文过长、角色冲突的问题,是 Agent 应用走向复杂化的必经之路。

第五层:生产级工程能力。围绕 Agent 建设完整的评估、监控、告警、灰度发布体系。目前业界公认“Agent 落地最大的瓶颈不是模型能力,而是评测与稳定性”。

学习过程中,强烈建议多读一些开源项目的源码,例如 LangChain 的 Agent Executor 实现,看官方代码是怎么处理循环、异常和记忆协作的。也建议关注 Karpathy 在 LLM 学习和知识管理方面的方法论,用工具化的方式管理自己沉淀的知识,这一点对 Agent 开发者来说尤其重要。

最后想说的是,AI Agent 是一个实践性极强的领域,光看教程不写代码,永远停留在“懂了但不会做”的状态。强烈建议你按照本文的案例,亲手敲一遍代码,然后试着给 Agent 增加一个“查新闻”或“发送邮件”的工具,你会真正找到感觉。

如果本文对你有帮助,欢迎收藏备用,也欢迎在评论区交流你的 Agent 实战踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询