不用框架,纯 Python 手搓 AI Agent 实战教程

作者:问题终结者2026.11.04 11:32浏览量:0

简介:本教程面向开发者与技术负责人,讲解如何不依赖任何 Agent 编排框架,仅用 Python 和大模型 API 从零实现一个具备工具调用、循环决策、记忆管理与安全终止能力的 AI Agent,内容涵盖选型分析、环境准备、核心代码实现、结果验证、常见问题排查与优化建议,帮助你构建更可控、更轻量、更易维护的智能体应用。

一、教程目标

本教程要解决一个很多团队都关心的问题:在真实工程项目中,如果不使用任何主流 Agent 编排框架,仅靠 Python 和大模型 API,能否从零实现一个真正可用的 AI Agent?

答案是肯定的。Agent 的本质并不复杂:它是一个“让大模型在循环中自主决策、调用工具、观察结果,直到完成目标”的程序。只要把这个循环写清楚,一两百行 Python 代码就足以支撑大量真实业务场景。完成本教程后,你将能够:

  1. 理解 AI Agent 的最小核心结构,看清各类框架封装之下到底是什么;
  2. 用纯 Python 实现一个具备工具调用、多轮循环、记忆管理与终止护栏的完整 Agent;
  3. 掌握验证、排查和优化的通用方法,把这套方案安全地推进到生产环境。

本教程适合有一定 Python 基础的开发者、负责技术选型的架构师,以及希望对 Agent 核心链路保持完全掌控的技术负责人。

二、适用场景与选型分析

先回答“可不可行”,再回答“值不值得”。

从原理上看,市面上各类 Agent 框架的核心能力都可以归结为四件事:调用大模型接口、管理消息历史、定义并执行工具、控制决策循环。这四件事分别对应 HTTP 请求、一个列表、一个字典和一个循环结构,没有任何一件是某个框架的“专利”。只要你所选的大模型服务支持函数调用能力,纯 Python 实现就没有原理性障碍。

从工程角度看,不引入框架往往带来以下收益:

  • 依赖更轻:无需安装庞大的依赖树,升级大模型 SDK 或 Python 版本时不受框架版本约束;
  • 链路更透明:每一步提示词拼装、每一次工具调用都发生在自己的代码里,排查问题不需要阅读框架源码;
  • 定制更自由:重试策略、上下文裁剪、日志埋点、成本统计都可以按业务需求精确控制;
  • 心智负担更低:团队只需理解“循环 + 工具”这一个模型,而不用学习某套框架特有的编排语法。

这套方案尤其适合以下场景:内部工具型助手(订单查询、工单处理、审批流转)、数据查询与分析助手、客服辅助问答、单链路的流程自动化等。需要客观说明的是,如果业务涉及复杂的多角色协作、动态图状编排,或团队希望在极短时间内验证原型,使用成熟框架可以省去部分底层工作。更合理的路径是:先用几十行代码把手搓原型跑通,确认业务形态后再决定是否引入更重的抽象。

三、前置准备

动手之前,请确认以下条件已就绪:

  1. Python 环境:建议 Python 3.10 及以上版本,会创建虚拟环境、安装依赖;
  2. 一个大模型 API:需支持函数调用能力,例如百度智能云提供的大模型服务,或其他任意具备该能力的大模型接口。获取方式通常是在对应云服务控制台开通服务并创建 API Key;
  3. 密钥管理:将接口地址、模型名称、API Key 统一放入环境变量,切勿硬编码到代码或提交进代码仓库;
  4. 基础知识:了解 HTTP 请求与 JSON 结构即可;异步编程、数据库为可选知识,不影响完成本教程;
  5. 一个真实工具:示例中以“查询订单状态”为演示工具,实际开发中可替换为你系统内的任意接口、数据库查询或内部服务调用。

agent-">四、Agent 的最小核心结构

写代码之前,先建立整体认知。一个最小可用的 Agent 由四个部分组成:

  • 大模型调用封装:把消息历史和工具描述发给大模型服务,并解析返回结果;
  • 工具注册表:一个“名称 → 函数”的字典,外加每个工具的结构化描述,供大模型理解何时调用、如何传参;
  • 消息历史:即 Agent 的短期记忆,记录系统提示、用户输入、模型决策和工具返回;
  • 主循环:驱动“模型决策 → 执行工具 → 结果回填 → 再次决策”的循环,直到任务完成或触发终止条件。

运行时的数据流可以概括为:

  1. 用户输入 → 组装消息与工具描述 → 调用大模型 → 返回的是工具调用请求吗?
  2. → 是:执行对应工具,把结果写回消息历史,进入下一轮循环
  3. → 否:模型输出最终自然语言回答,任务结束

理解了这个闭环,就理解了所有 Agent 框架的骨架。

五、实施步骤

步骤一:封装大模型调用

做什么:编写统一的 call_llm 函数,接收消息列表与工具描述,返回模型输出。
为什么:把接口调用收敛到一个函数里,后续无论更换模型、增加重试还是切换服务,都只需改这一处。
注意什么:必须设置超时;建议在此之上增加带指数退避的重试;密钥一律从环境变量读取。

  1. import json, os, urllib.request
  2. def call_llm(messages, tools):
  3. payload = json.dumps({
  4. "model": os.environ["MODEL_NAME"], # 模型名称
  5. "messages": messages, # 完整对话历史
  6. "tools": tools, # 工具描述列表
  7. }).encode("utf-8")
  8. req = urllib.request.Request(
  9. os.environ["LLM_API_URL"], # 大模型服务接口地址
  10. data=payload,
  11. headers={
  12. "Content-Type": "application/json",
  13. "Authorization": "Bearer " + os.environ["LLM_API_KEY"],
  14. },
  15. method="POST",
  16. )
  17. with urllib.request.urlopen(req, timeout=60) as resp:
  18. data = json.loads(resp.read().decode("utf-8"))
  19. # 以下为示意性解析,实际返回字段结构请以你所选大模型服务的接口文档为准
  20. return data["choices"][0]["message"]

步骤二:定义工具与结构化描述

做什么:把业务函数注册到字典中,并为每个工具编写结构化描述(函数调用是当前业界的通用规范)。
为什么:大模型本身不能执行你的业务逻辑,它只能“提出调用请求”。描述写得越清晰——工具做什么、参数含义、什么场景该用——模型选对工具、填对参数的准确率就越高。
注意什么:工具描述本质上属于提示词的一部分,值得像打磨提示词一样反复迭代;所有参数必须在服务端做校验。

  1. def get_order_status(order_id: str) -> str:
  2. # 真实场景中,这里会查询数据库或调用内部服务
  3. return json.dumps({"order_id": order_id, "status": "已发货"}, ensure_ascii=False)
  4. TOOLS = {"get_order_status": get_order_status}
  5. TOOL_SCHEMAS = [{
  6. "type": "function",
  7. "function": {
  8. "name": "get_order_status",
  9. "description": "根据订单号查询订单的当前物流状态。当用户询问订单进度时使用。",
  10. "parameters": {
  11. "type": "object",
  12. "properties": {
  13. "order_id": {"type": "string", "description": "订单编号,例如 SO-20240601-001"}
  14. },
  15. "required": ["order_id"],
  16. },
  17. },
  18. }]

步骤三:实现 Agent 主循环

做什么:编写核心循环,处理两种返回情况——工具调用请求与最终文本回答。本步骤依赖步骤一、二的成果。
为什么:这是 Agent 与普通对话机器人的本质区别。普通机器人一问一答即结束;Agent 会把工具结果喂回模型,让它基于真实数据继续推理,直到任务完成。
注意什么:执行工具时必须捕获异常,并把错误信息以工具结果的形式回传给模型,让模型有机会自我修正,而不是让整个进程崩溃。

  1. def run_agent(user_input: str, max_iterations: int = 8) -> str:
  2. messages = [
  3. {"role": "system", "content":
  4. "你是订单助手。涉及订单状态时必须调用工具获取真实数据,不要编造;"
  5. "拿到结果后用一句话总结,然后立即结束,不要重复调用。"},
  6. {"role": "user", "content": user_input},
  7. ]
  8. for _ in range(max_iterations):
  9. message = call_llm(messages, TOOL_SCHEMAS)
  10. messages.append(message)
  11. tool_calls = message.get("tool_calls")
  12. if not tool_calls: # 无工具调用,视为任务完成
  13. return message.get("content", "")
  14. for call in tool_calls:
  15. name = call["function"]["name"]
  16. args = json.loads(call["function"].get("arguments") or "{}")
  17. fn = TOOLS.get(name)
  18. if fn is None:
  19. result = json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False)
  20. else:
  21. try:
  22. result = fn(**args)
  23. except Exception as e:
  24. result = json.dumps({"error": str(e)}, ensure_ascii=False)
  25. messages.append({
  26. "role": "tool",
  27. "tool_call_id": call.get("id", ""),
  28. "content": result,
  29. })
  30. return "已达最大轮次,任务未能在限定步数内完成,请缩小问题范围后重试。"

步骤四:上下文与记忆管理

做什么:为消息历史增加容量控制。
为什么:多轮循环会让历史迅速膨胀,一旦超过模型上下文窗口,调用会直接失败,同时 token 成本也会快速上升。
常见做法:始终保留系统提示,仅保留最近若干轮对话;对更早的历史做摘要压缩;工具返回的原始数据较大时,只回传关键字段而非完整报文。是否需要跨会话的长期记忆取决于业务,初版可以不做。

步骤五:终止条件与安全护栏

做什么:在最大轮次之外,增加显式终止与行为约束。
为什么:循环失控是 Agent 最典型的事故来源,护栏必须在上线前就位,而不是出事后再补。
建议措施:设置轮次上限(示例为 8);限制单轮工具调用数量;在系统提示中明确“完成即停,同一参数不要重复调用”;对敏感工具增加人工确认开关;逐轮记录模型输出与工具结果,形成完整追踪日志。

步骤六:组装与首次运行

将前述代码组合为一个脚本,加入入口函数:

  1. if __name__ == "__main__":
  2. print(run_agent("帮我查一下订单 SO-20240601-001 现在到哪了"))

在开发环境中先以最小配置跑通,再逐步接入真实业务工具。

六、结果验证

运行后,按以下清单逐项核对,全部通过才算接入成功:

  1. 日志中能看到模型发起了 get_order_status 调用,且参数中的订单号与用户输入一致;
  2. 工具返回结果被正确写回消息历史,最终回答包含真实查询结果,而非模型编造的内容;
  3. 构造一个不存在的工具名或非法参数,Agent 能优雅报错并继续或终止,而不是进程崩溃;
  4. 将 max_iterations 临时改为 2 验证终止提示可以正常触发;
  5. 连续运行多轮不同问题,观察消息历史长度是否在可控范围内增长。

七、常见问题与排查

  1. 模型从不调用工具:检查工具描述是否含糊、系统提示是否明确“必须用工具获取真实数据”,以及请求体中的 tools 字段是否按所选服务的规范正确传递;
  2. 死循环或重复调用同一工具:确认轮次上限生效;在系统提示中加入“同一参数的调用不要重复发起”;必要时在代码层对相同调用做去重;
  3. 工具执行报错:优先检查参数类型转换与校验逻辑;观察错误信息回传后模型在下一轮是否自我修正,这本身就是调试 Agent 的重要信号;
  4. 上下文超长报错:启用历史截断或摘要;检查是否有工具返回了过大的原始数据,改为只回传必要字段;
  5. 成本超出预期:统计每轮 token 消耗,评估简单决策是否可交给更小的模型;对高频重复查询增加本地缓存;
  6. 偶发超时或限流:增加带指数退避的重试,并对并发请求数做限流。

八、优化建议

  • 性能:相互独立的工具调用可以并行执行;对响应速度敏感的场景启用流式输出;对确定性查询引入缓存层;
  • 安全:API Key 只存在于环境变量或密钥管理服务中;工具遵循最小权限原则;凡涉及生成 SQL、执行命令的工具,必须做白名单校验与只读隔离;
  • 稳定性:所有外部调用设置超时与重试;关键工具保证幂等;为 Agent 增加熔断开关,异常时降级为人工处理;
  • 可维护性:用结构化日志记录每一轮完整消息历史,形成可回放的追踪链路;工具函数与普通业务函数一样编写单元测试;系统提示与工具描述纳入版本管理;
  • 成本:按任务复杂度分级使用模型——简单意图识别用轻量模型,复杂推理再用能力更强的模型;控制上下文长度是最直接的成本手段。

九、总结

回到最初的问题:不用框架,纯 Python 手搓 AI Agent 可行吗?不仅可行,而且在依赖控制、链路透明、深度定制等方面,在许多真实工程场景下就是更优选择。本教程带你完成了从可行性分析、环境准备,到四大核心组件(大模型调用封装、工具注册表、消息历史、主循环)的实现,再到终止护栏、结果验证、问题排查与持续优化的完整闭环。

后续你可以在此基础上继续演进:引入流式输出改善交互体验、增加结构化追踪提升可观测性、探索多 Agent 协作模式,或建立自动化评估集来量化每次提示词与工具调整的效果。当你真正理解了“循环 + 工具 + 记忆”这个最小内核,无论未来使用哪种框架或平台,你都会清楚它们在替你做什么,以及什么情况下值得自己动手。