简介:本文系统定义“纯 Python 手搓 AI Agent”这一技术路线,剖析其核心组成、运行原理与适用场景,并对比框架化开发的区别。你将理解为何在多数生产环境中,直接基于大模型 API 构建智能体不仅可行,且能带来更高的可控性与调试效率,同时掌握规避常见陷阱的关键策略。
“纯 Python 手搓 AI Agent”指的是:不依赖任何第三方 Agent 开发框架或编排库,仅使用 Python 标准库、基础网络请求库(如 HTTP 客户端库)以及大语言模型(LLM)官方提供的 API 接口,从零开始构建具备自主规划、工具调用、记忆管理和结果反馈循环的智能体系统。其本质是开发者直接操控“模型输入(提示词)—模型输出(结构化结果)—代码逻辑判断—下一轮输入构造”这一完整闭环,而不是通过框架的封装层间接完成。
该方式强调两点:一是“无框架依赖”,即没有引入额外的抽象层管理 Agent 的状态机、上下文窗口或工具注册表;二是“原生 Python 驱动”,即所有控制流(循环、分支、异常处理)由开发者手写代码显式控制,而非由框架配置或声明式规则隐式决定。这种模式下,Agent 的“智能”主要来源于三部分:LLM 的语义理解与生成能力、开发者编写的策略代码(如何时终止循环、如何合并工具结果)、以及外部工具 API 的返回数据。
与其容易混淆的是“半手搓”模式,即使用轻量级 SDK(仅提供 API 封装,不提供 Agent 编排能力)发起请求,但自己实现 ReAct 循环、记忆存储和任务队列。需要强调的是,本文定义的“纯手搓”允许使用 LLM 厂商的官方客户端 SDK,因为其仅简化 HTTP 通信细节,不涉及 Agent 逻辑封装。
在大模型应用爆发初期,开发者普遍面临两个选择:使用高度封装的 Agent 框架快速搭建,或从零编写控制逻辑。框架的价值在于提供了开箱即用的组件——例如消息历史管理、工具调用解析、多智能体通信协议等。但随之而来的问题是抽象层过厚,掩盖了实际发送给模型的提示词内容和模型返回的完整响应,导致调试时开发者必须“穿透”多层封装,甚至需要阅读框架源码才能定位问题。
纯手搓方式直接回应了这一痛点。其核心价值体现在三个维度:
第一,透明性与可调试性。 当开发者自己维护对话循环时,每一轮发送给 LLM 的消息列表、工具调用的参数、模型返回的原始 JSON 或文本,都暴露在代码变量中。使用内置的日志工具即可完整回放 Agent 的每一步决策过程,无需依赖框架的追踪模块。
第二,最小化依赖与快速迭代。 一个手写 Agent 的核心循环通常只需几十行代码(定义工具函数、构造系统提示词、解析模型输出、执行工具、拼接结果)。这意味着代码库更精简,启动速度更快,且不会因框架版本升级导致行为变化。在生产环境中,这意味着更低的维护成本和更少的供应链风险。
第三,精准控制成本与延迟。 手写代码可以针对特定场景做极致优化,例如在工具调用成功且结果满足条件时跳过额外一轮模型推理,或根据工具结果动态缩短历史消息长度。这些策略在框架中往往难以细粒度定制。
一个生产可用的纯 Python Agent,无论场景多么简单,都必须包含以下五个核心模块。缺少任何一个,系统都会退化为普通的单轮对话接口。
1. 任务解析器(意图识别层)
负责任务拆解,但该拆解并非全由 LLM 完成。常见实现是:定义一个“主循环”,在循环内第一次调用 LLM,要求模型输出严格的 JSON 格式,包含“思考过程(可选)”、“下一步动作(调用工具或给出最终回答)”、“动作参数”。开发者使用 json 库解析模型输出,并针对 action 字段做 if-else 分发。
2. 工具执行器(函数注册表)
底层是一个 Python 字典,键为工具名,值为可调用函数。开发者需手写每个工具的函数签名和返回值说明,在发送给 LLM 的提示词中,用自然语言描述每个工具的功能、参数和返回值含义,并附带一个“工具使用示例”。工具执行器拿到 LLM 输出的参数(通常是 JSON 字符串)后,调用对应函数并捕获异常,将结果格式化为字符串。
3. 记忆管理器(上下文构造器)
该模块负责决定“将哪些数据放入当前轮次的提示词”。最简单的实现是维护一个 Python 列表,按顺序存储历史消息。重点在于“截断策略”:当历史消息总字符数超过预设阈值(如 8000 token)时,需要手写摘要逻辑(调用一次 LLM 总结之前的对话)或滑动窗口删除最旧消息。高级实现还包括短期记忆(当前任务上下文)与长期记忆(存储在本地文件或向量数据库中的运行日志)的分离,但纯手写通常先基于列表和文件存储实现。
4. 循环控制器(决策引擎)
这是 Agent 的核心逻辑。标准的 ReAct(Reason + Act)循环用伪代码表示如下:
for step in range(MAX_STEPS):response = llm.chat(messages_with_tools_description_and_history)parsed = parse_json(response)if parsed["action"] == "final_answer":print(parsed["answer"])breakelif parsed["action"] in tools_registry:result = tools_registry[parsed["action"]](**parsed["parameters"])messages.append({"role":"tool","content":str(result)})continueelse:# 处理异常:重新询问模型messages.append({"role":"user","content":"非法动作,请重新选择。"})
该控制器需处理三类边界情况:最大步数限制(防止无限循环)、工具调用失败重试、模型输出非 JSON 格式的容错(提取代码块中的 JSON)。
5. 输入输出适配层
负责处理外部系统的非标准数据。例如,当 Agent 需要调用一个需要身份认证的 REST API 时,适配层负责在调用前注入从环境变量读取的密钥;当工具返回的是图片二进制数据时,适配层需将其转换为 base64 字符串或降级为文本描述,再交给 LLM。
手写 Agent 的工作原理在宏观上是一个“感知—推理—行动—观察”的循环,但在微观实现层面,其本质是大量字符串拼接、解析与状态管理。
第一步:环境初始化。 代码定义系统提示词,其中注入 Agent 的角色描述、工具清单(包含每个工具的 JSON Schema 样式说明)、任务目标、输出格式约束(如必须输出 JSON)。
第二步:推理与输出。 将系统提示词与历史消息拼接为一个消息数组,调用 LLM 的 chat.completions 接口。模型返回一段文本。此时开发者不做任何假设,直接将该文本作为纯字符串处理。
第三步:结构解析。 使用正则表达式或字符串查找,定位第一个 { 和最后一个 } 之间的内容,尝试用 json.loads 解析。若失败,则作为异常分支处理——将错误信息返回给模型,要求其修正格式。
第四步:动作执行。 根据解析出的 action 字段查找函数映射表,将 parameters 字典展开为关键字参数调用工具函数。工具可能返回结构化数据(列表、字典)或纯文本。开发者需统一将其序列化为字符串,并添加前缀“工具执行结果:”。
第五步:上下文更新。 将工具返回的字符串以 user 角色(或 tool 角色,取决于所用 API 的规格)追加到消息列表。这构成了“观察”步骤。随后回到第二步,进入下一轮循环,直至模型输出包含 final_answer 标志。
第六步:终止与清理。 循环结束后,代码应将最终答案从 JSON 中抽取并输出。若达到最大步数限制仍未得到最终答案,则触发降级策略:手动拼装消息要求模型直接总结已知信息。
在此过程中,LLM 每次推理看到的是“历史对话 + 最新观察结果 + 可用工具清单”,它不拥有状态——状态完全由 Python 代码中的 messages 列表维护。这正是手搓方式与框架方式的本质区别:框架通常将状态封装在内部对象中,而手搓方式将状态显式暴露为普通数据结构。
纯手写 Agent 并非万能,在以下三类场景中,其优势尤为明显:
场景一:单任务、强约束的内部自动化工具。 例如,企业内部“数据库查询助手”:Agent 只需将自然语言转换为 SQL 查询(调用数据库驱动),执行查询,并将返回的表格数据转化为 Markdown 格式输出。该场景下工具数量少(通常 1-2 个),任务流程固定,无需复杂的状态转移。手写代码可确保每次查询前强制要求模型输出参数校验 JSON,且完全掌控 SQL 白名单校验逻辑,避免框架中的通用工具解析带来的注入风险。
场景二:需要细粒度成本控制的批处理系统。 例如,批量处理 10 万行日志的分类任务。手写代码可以严格控制每批次发送给 LLM 的上下文大小,并在单个分类结果置信度较高时,通过代码判断跳过“二次确认”推理步骤,从而节省约 20% 的 token 消耗。而框架往往会为统一性牺牲这种定制化优化空间。
场景三:集成复杂已有系统的嵌入式 Agent。 当 Agent 需要作为中间件嵌入到已有的异步任务队列或消息总线中时,手写方式更易与现有代码库融合。开发者只需将主循环封装为一个 Python 函数,通过 queue 接收任务,通过 callback 返回结果,无需引入框架自带的事件循环或并发模型。
为了更清晰地定义边界,需要将其与两类常见技术方案区分。
与框架化 Agent 开发的区别。 框架(如行业常见的多智能体编排方案)提供的是“声明式配置”或“图形化编排”,开发者通过 YAML 文件描述智能体行为。手写方案则是“命令式编程”,所有逻辑都是显式代码。一个直观差异是:若需要给 Agent 增加“当工具连续失败两次时,停止尝试并请求用户输入”,手写代码只需在循环中增加一行 if fail_count >= 2: break;而在框架中,这可能需要研究框架的容错钩子文档并编写自定义回调。
与工作流引擎(如事件驱动流程编排)的区别。 工作流引擎通常指预先定义好的有向无环图,节点之间的流转是固定的,不依赖 LLM 动态决断。手写 Agent 的循环则允许模型基于工具返回结果自主决定下一步动作。例如,在“网页信息收集”任务中,Agent 第一次调用搜索工具得到 5 个链接,它可以自主决定是逐个访问,还是先访问第一个再根据内容决定是否访问第二个。工作流引擎需要提前枚举所有可能的访问路径,这在复杂任务中几乎不可行。
尽管手写路线在可控性上优势明显,但落地时需谨慎规避以下陷阱。
陷阱一:提示词中出现代码逻辑。 禁止在系统提示词中要求模型执行复杂条件判断,例如“如果 A 大于 3 就调用工具 X,否则调用工具 Y”。正确做法是给模型简单的规则(“请根据结果选择最合适的工具”),并将复杂判断放在 Python 代码中,解析工具返回结果后进行 if 判断。
陷阱二:忽略工具的异常返回值。 工具可能因网络超时或权限不足返回不完整的字典。手写代码必须在调用每个工具后检查返回值的类型和字段完整性,否则会因后续代码访问不存在的键而抛出未捕获异常,导致 Agent 进程崩溃。
陷阱三:上下文无限膨胀。 如果不精简单轮工具返回结果,长任务中上下文很快会达到模型 token 上限。实践建议是:在将工具结果追加到消息列表中之前,先做长度裁剪(例如仅截取结果前 2000 字符),并强制在工具结果末尾添加“结果已截断”标记,让模型知晓信息不完整。
实施建议: 建议从“线性任务”开始验证可行性,即每一步工具调用都是独立的。先实现一个固定两轮循环的 Agent,再逐步放开循环次数;在集成外部 API 时,优先使用官方 SDK 进行请求,但将 SDK 封装在工具函数内部,避免 SDK 的类型渗透到 Agent 核心循环中;最后,务必为 Agent 的每一步循环添加日志(包括输入 token 数、输出 token 数、工具调用耗时),这些指标是后期优化成本与延迟的唯一依据。
纯 Python 手搓 AI Agent 是一种通过显式代码控制模型调用与工具编排的技术方案,其核心定义在于“无框架抽象、全流程可控”。它并非对框架的否定,而是一种基于不同权衡的工程选型。对于追求调试透明度、成本精细化控制、以及与现有系统深度集成的团队,该方案提供了坚实的实现路径。理解其核心组成(解析器、执行器、管理器、控制器、适配层)与工作循环,是驾驭这种开发模式的前提。在决定选型时,请关注团队的维护能力与任务复杂度——当任务涉及多种长期记忆策略或复杂的多智能体协商时,引入框架可能是更务实的选择;反之,简单直接的手写方案往往能带来更快的交付速度与更稳定的运行表现。