AI Agent 从零实现:大模型工具调用与主循环实战指南

AI Agent 从零实现:大模型工具调用与主循环实战指南 AI Agent 是现阶段 AI 应用开发中最值得投入时间的工程方向之一。单纯调用大模型接口只能生成文本而 Agent 可以围绕一个目标连续地思考、调用工具、观察结果并修正下一步行动这使它能够完成“查天气、算数据、读文档、写报告”这类需要多步协作的任务。这篇文章从概念讲起再带着你用 Python 从零实现一个最小可用 Agent最后给出运行验证、常见问题和生产化建议。顺着这条路径走完你既能理解 Agent 的运行逻辑也能动手写一个属于自己的 Agent 工作流。1. 先搞清楚 AI Agent 到底是什么以及为什么不能当普通 API 调用1.1 从“聊天机器人”到“有行动能力的 Agent”传统聊天机器人给人的印象是用户输入一句话模型返回一段文本。这个过程是一次性的、无状态的即使把上一轮对话拼进消息列表本质上仍然是在“续写”。聊天机器人没有目标不会主动判断“我现在缺什么信息”更不会去调用外部系统补齐这个信息。AI Agent 不同。它虽然也是基于大模型但工作方式已经变成接受一个目标把目标拆成步骤在每一步判断是直接回答还是调用工具获取必要信息然后把工具结果当作新的观察继续推进直到目标完成。可以这样理解聊天机器人像只会回答问题的新员工你问什么他答什么Agent 像能领到任务后自己查资料、跑数据、写材料、最终交付结果的老员工。这个区别不是产品包装上的差异而是系统设计上的差异。1.2 Agent 的四要素规划、记忆、工具、行动把 Agent 拆开看核心由四部分组成。要素通俗解释工程落地规划把大目标拆成可执行的小步骤让模型依据当前结果判断下一步动作可以用 prompt 或 planner 模块实现记忆记住已经发生的事和已经拿到的信息短期记忆直接存在消息列表里长期记忆需要向量数据库或文档索引工具让 Agent 能触达外部世界比如计算、查询、搜索、写文件把函数封装成工具描述通过模型返回的 tool call 触发行动执行工具并拿到结果再喂回给模型在主循环里调用工具函数把结果追加为 tool 消息这四个要素互相配合。规划负责“想”行动负责“做”记忆负责“记录”工具负责“连接外部”。缺了工具Agent 只能输出计划缺了记忆它会在多步任务中丢失前文缺了规划它只会机械执行缺了行动它就退回了聊天机器人。1.3 Agent 的典型运行循环感知-决策-行动-观察Agent 的运行方式通常用一个循环描述感知输入决策下一步执行行动观察结果再回到决策。这个循环在学术上常被称为 ReAct核心思想是让模型“先想一步再走一步”。具体到工程实现用户输入被组装成消息列表这是 Agent 的“初始感知”。模型根据消息列表和可用工具决定是直接输出最终答案还是调用某个工具。如果模型决定调用工具程序就执行对应函数拿到返回值。工具返回值被追加到消息列表作为“观察结果”。带着新的观察结果再次调用模型开始下一轮决策。重复这个过程直到模型不再申请调用工具输出最终回答。为什么一定要循环因为大模型本身没有计算能力不知道当前时间也无法访问业务数据库。它只能根据输入文本生成下一步动作真正的数据获取必须由外部函数完成。循环让“模型思考”和“工具执行”交替进行最终逼近目标。2. 从零搭建 Agent 开发环境选择技术栈和依赖2.1 技术选型为什么先用 Python 和最小依赖开发 AI Agent 的语言选型以 Python 居多原因是大模型 SDK、向量数据库客户端、数据处理库都优先提供 Python 版本生态最完整。对于刚接触 Agent 的开发者不要一开始就引入 LangChain、LlamaIndex 等重型框架而是先用最少的依赖把主循环写明白。不是因为这些框架不好而是因为框架封装了太多细节。一旦项目报错你很难判断是模型问题、消息结构问题还是框架版本问题。先用 OpenAI SDK 把最小闭环跑通再逐步引入框架是更稳妥的学习路径。2.2 环境准备Python、虚拟环境和依赖建议使用 Python 3.10 及以上版本避免低版本在类型注解和异步库上踩坑。先创建项目目录和虚拟环境。mkdir ai-agent-demo cd ai-agent-demo python3 -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate虚拟环境激活后创建依赖文件。pip install openai1.30.0 python-dotenv1.0.0把依赖写入 requirements.txt方便别人复现。openai1.30.0 python-dotenv1.0.0这里只用两个依赖一个负责调用大模型接口一个负责读取环境变量。后续如果要加日志、数据库、向量检索再按需引入。2.3 模型接口OpenAI 兼容接口的配置方式Agent 主循环只依赖一个能力给定消息列表和工具描述返回文本或工具调用请求。OpenAI SDK 把这种能力封装成了 chat completion 接口而且很多本地部署的推理服务也提供 OpenAI 兼容接口。因此项目里只需要配置 API Key、Base URL 和模型名即可。项目根目录创建.env.example作为配置模板。LLM_API_KEYsk-your-key LLM_BASE_URLhttps://api.your-provider.com/v1 LLM_MODELgpt-4o-mini MAX_ITERATIONS10使用时复制为.env并填入真实配置。cp .env.example .env如果你的模型来自本地服务或私有化部署只需要把LLM_BASE_URL换成对应服务的地址LLM_MODEL换成实际模型名代码主体不用改动。这种兼容层的价值在于Agent 逻辑与具体模型供应商解耦切换模型时只改环境变量。2.4 项目目录结构这个 Demo 不准备做得太复杂保持单个 Python 文件即可。ai-agent-demo/ ├── .env.example ├── .env ├── requirements.txt └── agent.py.env中包含密钥不能提交到 Git。生产环境应当使用密钥管理服务而不是把密钥写进环境变量文件。但学习阶段.env已经足够。3. 实现一个最小可运行的 Agent任务助手3.1 设计 Agent 的数据结构和状态Agent 的“记忆”基础是消息列表。OpenAI 兼容接口的消息类型主要有四种system系统提示词告诉模型行为方式。user用户输入。assistant模型返回的内容可能包含普通文本和工具调用请求。tool工具执行后返回的结果。一次完整的工具调用在消息列表里会形成三块内容模型请求调用某个工具的 assistant 消息、包含工具结果的 tool 消息、以及模型基于工具结果生成的下一条 assistant 消息。工具结果必须通过tool_call_id和之前的调用请求对应起来否则接口会报错。还需要维护工具注册表。简单做法是用字典保存工具名到函数的映射同时为每个工具写一份 JSON Schema 描述。这样在调用模型时可以直接把描述传给tools参数。3.2 封装 LLM 调用在agent.py中先加载环境变量并创建客户端。import os import json from datetime import datetime from openai import OpenAI from dotenv import load_dotenv load_dotenv() client OpenAI( api_keyos.getenv(LLM_API_KEY), base_urlos.getenv(LLM_BASE_URL), ) MODEL os.getenv(LLM_MODEL, gpt-4o-mini) MAX_ITERATIONS int(os.getenv(MAX_ITERATIONS, 10))核心请求方法如下。def call_model(messages, tools): response client.chat.completions.create( modelMODEL, messagesmessages, toolstools, tool_choiceauto, ) return response.choices[0].message重点在于tools参数和tool_choice。tool_choiceauto表示让模型自己决定是否调用工具也可以强制模型必须调用某个工具但最小 Demo 中auto最合适。3.3 注册工具给 Agent 一双“手”为了让 Agent 展示“能行动”的价值添加两个工具获取当前时间、计算数学表达式。先定义工具描述。TOOLS [ { type: function, function: { name: get_current_time, description: 获取服务器当前时间返回字符串, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: calculate, description: 计算一个简单的数学表达式例如 12返回计算结果, parameters: { type: object, properties: { expression: { type: string, description: 要计算的数学表达式 } }, required: [expression] } } } ]再实现对应函数。def get_current_time(): return datetime.now().strftime(%Y-%m-%d %H:%M:%S) def calculate(expression): # 注意这里使用 eval 仅用于演示生产环境必须换成安全表达式解析器 try: return str(eval(expression, {__builtins__: {}}, {})) except Exception as e: return f计算失败: {e}这里要特别说明eval在生产环境有严重安全风险不能直接暴露给用户输入。示例代码只是为了展示工具调用链路真实项目应使用ast解析表达式或专用计算库。3.4 Agent 主循环主循环是整篇文章最核心的部分。它的职责是把消息发给模型判断模型是否要调用工具执行工具把结果塞回消息列表然后继续下一轮。def run_agent(user_message): messages [ {role: system, content: 你是一个可以调用工具完成任务的助手。}, {role: user, content: user_message}, ] for step in range(MAX_ITERATIONS): print(f\n[Step {step 1}] 调用模型...) message call_model(messages, TOOLS) messages.append(message) if not message.tool_calls: print([Agent] 最终回答:, message.content) return message.content for tool_call in message.tool_calls: fn_name tool_call.function.name fn_args json.loads(tool_call.function.arguments or {}) print(f[Tool] 调用 {fn_name}, 参数: {fn_args}) if fn_name get_current_time: tool_result get_current_time() elif fn_name calculate: tool_result calculate(fn_args[expression]) else: tool_result f未知工具: {fn_name} messages.append({ role: tool, tool_call_id: tool_call.id, content: str(tool_result), }) print([Agent] 达到最大迭代次数停止。) return None这个循环里有两个关键点。第一模型返回的message必须原样追加到messages因为里面可能携带tool_calls后续消息列表需要保留这个上下文。第二工具结果必须使用与工具调用相同的tool_call_id否则接口无法匹配。3.5 完整代码组合把上面的代码组合进同一个文件并加入交互入口。if __name__ __main__: print(AI Agent Demo 已启动输入 exit 退出。) while True: user_input input(你) if user_input.strip().lower() in {exit, quit}: break run_agent(user_input)完整agent.py结构就是环境加载、客户端创建、工具定义、工具实现、模型调用、主循环、交互入口。这个文件已经构成一个可运行的最小 Agent。4. 运行 Agent 并验证它真的会“用工具”4.1 启动前的配置检查运行前先确认三件事.env中LLM_API_KEY、LLM_BASE_URL、LLM_MODEL是否配置正确虚拟环境已激活依赖已安装。然后启动。python agent.py如果模型服务连接正常终端会显示交互提示。如果出现连接错误或鉴权失败优先检查LLM_BASE_URL末尾是否带/v1以及LLM_API_KEY是否和模型服务商匹配。4.2 验证普通问答场景输入你你好请介绍一下你自己预期输出是模型直接回答日志中不会出现[Tool]调用记录。[Step 1] 调用模型... [Agent] 最终回答: 你好我是一个可以通过调用工具完成任务的AI助手。这个场景的意义是确认基础聊天链路可用。如果连普通问答都失败说明模型接口配置有问题不需要继续测工具。4.3 验证计算类问题输入你请帮我计算 12345 * 6789预期输出会先调用 calculate 工具再基于工具结果回答。[Step 1] 调用模型... [Tool] 调用 calculate, 参数: {expression: 12345 * 6789} [Step 2] 调用模型... [Agent] 最终回答: 12345 * 6789 的结果是 83810205。如果模型没有调用工具而是直接给出了结果不要急着认为代码有问题。部分模型在简单乘法上会直接输出幻觉结果这时可以显式在 system prompt 中强调“遇到需要精确计算的任务必须调用 calculate 工具”再重新尝试。4.4 验证一次请求发起多个工具调用输入你现在几点顺便算一下 (23)*4这个场景包含两个子问题。模型可能在第一步就同时返回两个tool_calls也可能先调用一个再调用另一个形成多轮循环。两种行为都属于正常范围取决于模型对任务的理解。[Step 1] 调用模型... [Tool] 调用 get_current_time, 参数: {} [Tool] 调用 calculate, 参数: {expression: (23)*4} [Step 2] 调用模型... [Agent] 最终回答: 当前时间是 2026-01-08 10:30:00(23)*4 的结果是 20。这里要注意示例代码中的for tool_call in message.tool_calls会依次执行同一轮模型返回的所有工具调用然后把所有工具结果一起追加到消息列表再进入下一轮模型调用。这种实现可以处理多工具并行场景。4.5 观察日志确认调用链路运行验证的核心不是看最终回答而是看调用链路是否符合预期。日志中的[Step N]、[Tool]就是判断依据。如果你要排查 Agent 为什么没有调用工具第一眼应该看日志里有没有出现[Tool]而不是直接看最终结果。建议在实际项目中增加更详细的日志至少记录每一步的 token 消耗、工具执行耗时、消息列表长度。5. 主循环细节、参数和常见误区5.1 关键参数速查Agent 主循环中除了消息结构参数对行为影响也很大。下面几个需要重点理解。参数含义默认值调大影响调小影响推荐用法temperature采样随机性1.0回答更多样但更容易跑偏回答更稳定但可能机械工具调用场景建议 0 到 0.3max_tokens单次模型输出最大 token 数视模型而定能输出长答案但成本增加可能截断答案按业务回答长度设置tool_choice是否让模型调用工具auto强制调用工具会忽略普通回答关闭工具调用则退化为聊天默认 auto特殊场景用 requiredMAX_ITERATIONSAgent 最多循环轮数10允许更长任务链提前结束可能完不成任务学习环境 5 到 10生产按成本评估对于 Agent 类任务temperature通常设置为较低的数值。工具调用本质是执行确定性操作随机性过高会导致模型一会儿调工具、一会儿不调工具难以稳定复现。5.2 最大迭代轮数与死循环初学者最容易踩的坑是Agent 反复调用同一个工具但每次参数几乎不变形成死循环。比如模型请求calculate计算某个表达式工具返回了结果但模型仍然继续请求同一个计算直到轮数耗尽。出现这个现象的原因通常是工具结果没有被模型正确理解模型可能忽略了tool消息。system prompt 没有说明何时停止调用工具。历史工具结果过长被模型截断或遗忘。模型本身对任务规划能力较弱。解决方案有三个层面。第一设置合理的MAX_ITERATIONS不要让 Agent 无限循环。第二在 system prompt 中加入停止条件比如“当你已经获得足够信息并能回答用户时直接输出最终答案不要再调用工具”。第三在工具返回值中加入简短的下一步建议帮助模型收敛。5.3 工具调用异常处理工具执行阶段可能发生异常比如参数缺失、超时、外部 API 报错。示例代码虽然用try except包裹了calculate但这只是最低保障。更稳妥的做法是工具函数内部捕获异常把错误信息作为字符串返回而不是抛给主循环。主循环对未知工具名做兜底处理。日志记录错误详情方便后续定位。关键原则是Agent 主循环不要因为单个工具失败而整体崩溃。工具失败是正常现象模型应该看到失败原因后调整策略比如换一个工具、修改参数或者直接告诉用户无法完成。5.4 日志和追踪极小 Demo 可以只print但进入生产环境必须有结构化日志。推荐的日志字段至少包括{ request_id: 7f3a2f, step: 3, model: gpt-4o-mini, input_tokens: 1200, output_tokens: 300, tool_name: calculate, tool_duration_ms: 15, message_count: 8 }有了这些字段才能在出问题时回答“是哪一步、调了什么工具、用了多少 token、为什么失败”。没有日志的 Agent 就像没有监控的线上服务排查成本会成倍增加。6. 常见问题排查从现象定位根因6.1 Agent 完全不调用工具现象无论用户问什么模型都直接回答日志中始终没有[Tool]记录。可能原因和排查路径可能原因检查方式处理建议tools 参数未传或格式错误打印传给模型的 tools 参数按 OpenAI 格式重新构造模型版本不支持 function calling查阅模型文档确认是否支持工具调用换成支持工具调用的模型temperature 过高查看当前 temperature 设置调低到 0 或 0.2prompt 没有要求使用工具检查 system prompt增加“如果需要必须调用工具”用户问题确实不需要工具换一个明确需要计算或查询的问题测试用“计算 12345*6789”验证6.2 工具执行后模型无法继续现象日志显示工具已执行但下一轮模型调用报错或者模型直接停止。最常见原因是消息结构问题。工具结果追加时必须包含role: tool并且携带正确的tool_call_id。如果漏掉tool_call_id接口会提示消息对应关系错误。检查方法把完整的messages打印出来确认每一轮 assistant 消息中的tool_calls和后续 tool 消息的tool_call_id一一对应。6.3 上下文超限现象多轮循环后报context length exceeded错误。原因多轮 Agent 任务会把每次工具结果都保存在消息列表里随着循环次数增加token 快速增长。解决方案限制最大迭代轮数。对长工具结果做截断只保留摘要。把历史消息压缩成 summary再拼入消息列表。换用支持更长上下文的模型。6.4 工具结果解析失败现象模型返回的arguments不是合法 JSONjson.loads抛异常。可能原因模型输出的arguments带有额外说明文字或者使用了单引号、换行等不规范格式。解决方案使用try except包裹解析逻辑。把expression参数设计成更严格的格式要求。在函数描述中写明“参数必须是合法 JSON 对象”。这类问题在部分模型上比较常见生产环境需要做容错。6.5 API 超时和限流现象请求长时间无响应或返回 429、503 错误。处理建议在客户端设置timeout参数。对瞬时错误做指数退避重试。控制并发请求数量。记录失败次数触发降级逻辑。7. 从 Demo 到生产环境还差哪些能力7.1 学习环境与生产环境的差距示例代码能跑通但离生产环境还有很大距离。核心差距不在于“代码写得不够高级”而在于运行条件不同。维度学习 Demo生产环境用户规模单用户命令行多用户并发密钥管理本地 .env密钥管理系统可观测性print 日志结构化日志、链路追踪工具安全eval 演示白名单、权限校验、审计记忆持久化内存消息列表数据库 / 向量存储模型容错没有重试重试、降级、熔断成本控制不关注 token需要配额和预算7.2 生产 Agent 需要补齐六个能力第一配置外置。模型名、API 地址、工具开关等都要支持运行时配置不能改代码才能换环境。第二重试与降级。模型接口、外部工具都可能失败需要设计重试策略并在模型不可用时降级到备用模型或备用方案。第三安全与权限。Agent 的工具调用能力越强越要严格校验。哪些人可以触发哪些工具、工具操作是否被审计都要纳入设计。第四可观测性。每轮运行都要有 trace能回答“用户从进入到完成Agent 经历了哪些步骤”。第五记忆持久化。生产环境不能只靠消息列表需要把重要信息存入数据库或向量检索服务。第六版本管理。prompt、工具定义、模型参数都属于可迭代的资产需要像代码一样管理版本。7.3 从单 Agent 到多 Agent示例是一个单 Agent 循环。随着任务变复杂可以拆成多个角色比如规划 Agent 负责拆解任务执行 Agent 负责调用工具质检 Agent 负责检查结果。多 Agent 的关键不是代码更复杂而是任务如何流转、结果如何传递、谁负责最终输出。建议先确保单 Agent 稳定再拆分角色否则排错难度会成倍增加。7.4 一个可复用的 Agent 开发检查清单下面这份清单适合在每次开发新 Agent 前对照检查。是否明确 Agent 要完成的目标和结束条件。是否列出 Agent 真正需要用到的工具避免过度暴露能力。是否设置最大迭代轮数防止死循环。是否处理工具异常并让模型看到错误信息后可以修正。是否记录足够的日志包括 step、工具、耗时、token。是否验证了至少一个普通问答分支和一个工具调用分支。是否检查了消息列表中 assistant 和 tool 的 tool_call_id 对应关系。是否评估了 token 成本避免长任务无限扩张。是否做了安全审查特别是工具是否会被滥用。这份清单不是模板而是每次开发时真正要过的关卡。每一项背后都对应一个线上事故类型。回到本文的核心判断AI Agent 并不是学术概念而是一种可以自己动手实现和验证的工程模式。这篇文章从一个最小循环出发把 LLM 调用、工具注册、多轮循环、异常处理串在了一起。建议你先把示例代码跑通再替换成一个真实业务工具比如查询数据库或调用内部 API你会在替换工具的过程中真正理解 Agent 的边界和坑。下一步可以继续研究记忆管理、规划算法和 Agent 框架但底层思想仍然是“感知-决策-行动-观察”这条主循环。把主循环理解清楚再去看任何 Agent 项目都会清晰很多。