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 "openai>=1.30.0" "python-dotenv>=1.0.0"把依赖写入 requirements.txt,方便别人复现。
openai>=1.30.0 python-dotenv>=1.0.0这里只用两个依赖,一个负责调用大模型接口,一个负责读取环境变量。后续如果要加日志、数据库、向量检索,再按需引入。
2.3 模型接口:OpenAI 兼容接口的配置方式
Agent 主循环只依赖一个能力:给定消息列表和工具描述,返回文本或工具调用请求。OpenAI SDK 把这种能力封装成了 chat completion 接口,而且很多本地部署的推理服务也提供 OpenAI 兼容接口。因此,项目里只需要配置 API Key、Base URL 和模型名即可。
项目根目录创建.env.example,作为配置模板。
LLM_API_KEY=sk-your-key LLM_BASE_URL=https://api.your-provider.com/v1 LLM_MODEL=gpt-4o-mini MAX_ITERATIONS=10使用时复制为.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_key=os.getenv("LLM_API_KEY"), base_url=os.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( model=MODEL, messages=messages, tools=tools, tool_choice="auto", ) return response.choices[0].message重点在于tools参数和tool_choice。tool_choice="auto"表示让模型自己决定是否调用工具,也可以强制模型必须调用某个工具,但最小 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": "计算一个简单的数学表达式,例如 '1+2',返回计算结果", "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 验证一次请求发起多个工具调用
输入:
你:现在几点?顺便算一下 (2+3)*4这个场景包含两个子问题。模型可能在第一步就同时返回两个tool_calls,也可能先调用一个,再调用另一个,形成多轮循环。两种行为都属于正常范围,取决于模型对任务的理解。
[Step 1] 调用模型... [Tool] 调用 get_current_time, 参数: {} [Tool] 调用 calculate, 参数: {'expression': '(2+3)*4'} [Step 2] 调用模型... [Agent] 最终回答: 当前时间是 2026-01-08 10:30:00,(2+3)*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.3 |
| max_tokens | 单次模型输出最大 token 数 | 视模型而定 | 能输出长答案,但成本增加 | 可能截断答案 | 按业务回答长度设置 |
| tool_choice | 是否让模型调用工具 | auto | 强制调用工具会忽略普通回答 | 关闭工具调用则退化为聊天 | 默认 auto,特殊场景用 required |
| MAX_ITERATIONS | Agent 最多循环轮数 | 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.2 |
| prompt 没有要求使用工具 | 检查 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不是合法 JSON,json.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 项目都会清晰很多。