简介:本文深入探讨不依赖任何框架、仅用Python与LLM API构建AI Agent的可行性。你将了解框架解决了哪些真实问题、何时应当放弃框架,以及如何用原生Python实现工具调用、循环控制与状态管理。适合希望提升调试效率与系统灵活性的开发者阅读。
在启动一个 LLM 应用项目时,团队几乎都会面临同一个决策:要不要引入 Agent 框架?
这个问题的答案并不像很多技术选型那样“看情况”。在相当多的真实工程项目中,不依赖任何编排框架,仅用 Python 标准库加上 LLM API,手动实现 AI Agent,不仅是完全可行的,而且在可维护性、调试效率和长期灵活性上往往是更优解。
支撑这一判断的并非个人偏好,而是来自一线工程团队的实践经验:有公司在生产环境使用某类 Agent 框架长达一年后,选择将其完全移除,并反馈“移除之后团队更快乐、也更高效”。这个结论值得深究——为什么一个初衷是提升效率的工具,最终会成为负担?
核心原因可以用一句话概括:LLM 应用在本质上只需要三样东西——字符串处理、API 调用和循环控制,而这些恰恰是 Python 原生就极其擅长的。当框架在这三者之上叠加了额外的抽象层,调试难度和灵活性损耗就会随之上升。
理解这个问题,可以先看主流模型厂商的官方立场。它们的态度形成了一个耐人寻味的光谱。
最为谨慎的一派明确建议从直接调用 API 开始。 其权威技术文档中指出:框架通过简化标准底层任务让入门变得容易,但它们往往会创建额外的抽象层,遮蔽底层的提示词和响应内容,使调试变得更加困难;同时,它们容易诱使开发者在简单方案就足够的情况下引入不必要的复杂性。该文档给出的建议非常直接——“从直接使用 LLM API 开始,许多模式只需几行代码就能实现。”文档还特别警告:对框架底层行为的错误假设,是客户问题的常见来源。
另一派则提供自研的轻量级 SDK,强调“代码优先”的方法,避免过度抽象,但依然鼓励使用官方封装。
还有一派支持多框架互操作,不绑定特定方案。
最为推崇框架使用的一派则明确表示“使用框架通常是更好的选择”。
这个光谱本身就说明了一件事:框架并非必需品,而是一种权衡。当连创造底层模型的厂商都在提醒你警惕抽象层时,这个提醒值得认真对待。
要做出理性判断,必须先承认框架的价值。它们并非凭空出现,而是针对真实痛点设计的。
第一类痛点是接口不统一。 不同模型供应商的 API 格式各异,请求结构、消息格式、工具调用协议都有差异。框架提供统一的模型接口,理论上可以避免供应商锁定。
第二类痛点是单次调用无法完成复杂任务。 真实业务往往需要多步骤串联——先检索、再推理、再调用外部系统、最后汇总。框架用链式或图式抽象来表达这种多步骤流程。
第三类痛点是模型无法执行实际动作。 模型只能生成文本,要让它查数据库、发请求、写文件,就需要工具调用机制。框架提供了工具注册与调度的标准接口。
第四类痛点是状态管理。 多轮对话、长任务的中间状态、断点恢复,都需要一套状态管理机制。部分框架提供状态图编排、条件分支、检查点和人工介入等高级能力,用于表达复杂分支逻辑和长运行任务的恢复。
这些问题都是真实存在的。但关键在于:解决这些问题所需的代码量,远比大多数人想象的少。
一个具备工具调用能力的 Agent,其核心逻辑可以用一个循环概括:
import jsondef run_agent(client, tools, system_prompt, user_input, max_steps=10):messages = [{"role": "system", "content": system_prompt},{"role": "user", "content": user_input},]for _ in range(max_steps):response = client.chat(messages=messages, tools=tools)msg = response.messagemessages.append(msg)# 没有工具调用请求,说明任务结束if not msg.tool_calls:return msg.content# 依次执行工具,把结果回填进对话for call in msg.tool_calls:fn = TOOL_REGISTRY[call.name]result = fn(**json.loads(call.arguments))messages.append({"role": "tool","tool_call_id": call.id,"content": str(result),})raise RuntimeError("超过最大步数,任务未完成")
这段代码不到三十行,却已经包含了 Agent 的全部本质要素:循环、工具调度、消息历史、终止条件。
工具注册也不过是一个字典:
TOOL_REGISTRY = {"search_docs": search_docs,"run_query": run_query,"send_notification": send_notification,}
状态管理则可以直接用列表、字典或数据类来表达,需要持久化就序列化成 JSON 落盘,需要断点恢复就读回来。这些操作不需要任何框架,Python 的标准库已经足够。
场景一:调试黑盒。 当框架在内部自动拼接提示词、自动裁剪历史、自动注入系统消息时,你看到的输入和模型实际收到的输入之间就产生了隔阂。一旦输出不符合预期,你需要在框架源码里逆向追踪,而不是直接打印两行日志。在原生实现中,print(messages) 就能看到全部真相。
场景二:版本升级破坏兼容。 框架的抽象层会随版本演进而调整。一次小版本升级可能改变回调签名、改变消息对象的字段名、改变默认的重试策略。对于已经上线的系统,这类变更意味着额外的回归成本和潜在故障。
场景三:为简单需求引入复杂依赖。 如果业务只需要“调用模型 + 解析 JSON + 重试三次”,引入一个包含数十个模块的框架,会让依赖树、启动时间和安全审计面都无谓地膨胀。
场景四:抽象与实际需求错位。 框架预设的执行模型未必匹配你的业务。当你想插入一个自定义的缓存策略、想在特定步骤做审计埋点、想用非标准的方式并行多个工具调用时,你往往要花更多精力去“绕过”框架,而不是直接实现。
保持中立是有必要的。以下情况框架可能带来实际收益:
但即便在这些场景,也建议先用原生方式实现一版最小可用原型,明确自己真正需要哪些能力,再决定是否引入框架,以及引入哪一层抽象。先理解问题,再选择工具,而不是反过来。
如果决定走原生路线,以下几点能让代码在长期维护中保持健康:
将提示词作为独立资源管理。 不要把提示词硬编码在函数里,而是抽成模板文件或常量模块,便于版本对比和灰度调整。
明确区分“模型调用层”和“业务编排层”。 前者封装 API 请求、重试、超时、限流;后者只关心步骤与分支。这样更换模型供应商时只需改动一层。
为每一步记录结构化日志。 记录输入消息、耗时、token 消耗、工具调用参数与返回。这些数据是后续优化和排障的基础,原生实现让你可以完全掌控日志字段。
用类型注解和数据结构约束边界。 工具函数的入参出参、消息对象的字段,都用数据类或类型标注固定下来,让错误在开发期暴露而不是运行期。
为循环设置硬性上限与超时。 任何自主循环都必须有最大步数和总时长限制,避免失控调用带来的成本风险。
测试时把模型调用替换为桩函数。 由于业务逻辑与模型调用已经解耦,你可以用固定响应模拟各种分支,实现快速、稳定的单元测试——这在重度依赖框架的场景中往往更难做到。
不用框架、纯用 Python 手搓 AI Agent,不仅可行,而且在多数工程场景下是更理性的起点。 框架解决的接口统一、多步编排、工具调用、状态管理等问题,其原生实现成本远低于普遍认知;而框架带来的抽象遮蔽、升级风险、依赖膨胀和灵活性约束,却是长期存在的实际成本。
理性的路径是:从直接调用 API 开始,用最少的代码把核心循环跑通,在真实需求出现时再逐层添加自己完全理解的能力。当复杂度确实增长到需要专门工具时,你会清楚地知道自己需要什么,也更有能力判断某个抽象是否值得引入。
工具应当服务于理解,而不是替代理解。在 LLM 应用这个仍在快速演进的领域,保持对底层机制的掌控,往往比拥抱现成抽象更能带来长期回报。