ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手撸Agent:十行代码跑通大模型API调用

从零手撸Agent:十行代码跑通大模型API调用 最近我给自己定了个小目标从零手撸一个 Agent。所谓从零不是直接用 LangChain、AutoGPT 那种现成框架而是从最底层的大模型 API 调用开始一行行代码自己写。这篇文章就是系列第一篇先把最小通路打通。不管框架怎么包装Agent 的底层逻辑都绕不开一件事它得能和大模型对话。模型负责思考代码负责调度两边通过 API 对接上Agent 才算有了最基本的形态。所以我决定先跑通最小路径——用尽量少的代码发起第一次大模型调用先拿到一个 response再考虑循环、规划、工具调用这些复杂功能。这篇文章就是这次实践的过程记录十行核心代码、四个踩过的坑、几个排查习惯原原本本写出来。适合想入坑 Agent 开发、又不想一上来就背一堆框架概念的同学参考。1. 先把目标说清楚为什么从零手撸1.1 Agent 的本质一次“循环”的智能体先把我对 Agent 的理解说清楚。大模型本身其实只是一个“很会说话的模型”你问它答仅此而已。但 Agent 不一样它是在模型之外加上目标、循环和工具让模型可以自主去完成一件具体事情的程序。如果用一张流程来描述一个最小可用的 Agent 长这样拿到用户目标拆解成计划调用外部工具获取信息根据信息决定下一步行动循环往复直到目标完成。这个流程里每一步的中间结果都要喂给大模型所以模型和代码之间的对话通道是整个 Agent 的地基。这也是我一直坚持的观点学习 Agent 的正确姿势不是先背框架概念而是先把“模型能回到话”这个最小路径跑通。框架只是把上面这个循环封装好了底层仍然是一次次的大模型 API 调用。自己先把调用写明白再去看各种框架就会有“原来如此”的通透感。1.2 动手前先立个 Flag十行代码动手之前我给自己定了一个很具体的验收标准能用不超过十行核心代码发起一次对话补全请求并在终端看到模型的回答。之所以把范围卡得这么死是因为我特别容易在“准备阶段”陷入内耗。装环境、配模型、研究框架每件事看起来都很重要但很容易让人忘记最重要的目标——先跑通。所以这次我把目标压缩到最小可运行的程度就算代码再丑、配置再简陋只要能在终端打印出模型的返回文本第一步就算赢了。这也算是给所有想入门 Agent 开发的同学一个建议先别纠结 Agent 到底应该有几个模块、要不要用数据库、要不要接向量检索先把“程序和大模型说上一句话”这件事做完后面的一切才有依附。2. 环境准备与十行核心代码2.1 准备工作依赖、密钥、模型选择第一件事把 Python 环境准备好3.10 以上版本都行。然后安装依赖这步很简单pip install openai python-dotenv这里解释一下这两个库的用途。openai 是官方 SDK封装了所有 HTTP 请求细节我们只需要关心传参和拿结果不用自己拼接 HTTP 请求体。python-dotenv 则是用来读 .env 文件的方便管理 API Key避免把密钥硬编码进代码里后面坑一还会详细讲到。然后去模型服务商的后台申请一个 API Key创建项目时把 Key 复制下来放到项目根目录的 .env 文件里OPENAI_API_KEYsk-xxxxxxxxxxxxxxxx这里有个值得注意的细节不同服务商提供的模型接口可能不完全一样但只要兼容 OpenAI 的 Chat Completions 协议代码逻辑基本是通用的只需要改 base_url 和 api_key 就行。所以在选择模型时不用太纠结先用一个响应快、成本低的模型跑通流程比如 gpt-4o-mini 或者 qwen-turbo 这类都在这个协议体系内。2.2 十行核心代码的完整实现环境准备好之后核心代码其实就这么多我一行行拆给你看import os from dotenv import load_dotenv from openai import OpenAI load_dotenv() client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: 用一句话介绍你自己}], ) print(resp.choices[0].message.content)这段代码正常情况下会直接在终端打印出一句话比如“我是一个大语言模型可以回答问题、提供建议和帮助创作内容”。下面把这十行按功能拆成三部分来看第一部分是配置加载。import os、from dotenv import load_dotenv、from openai import OpenAI 三行是导入要用到的库load_dotenv() 负责把 .env 文件里的变量加载到环境变量中client OpenAI(...) 创建客户端实例api_key 从环境变量里取。第二部分是发起请求。create 方法接收两个核心参数model 指定模型名messages 则是一个消息列表。messages 的格式特别重要它是一个数组每个元素必须包含 role 和 content 两个字段role 表示消息角色content 是消息内容。第三部分是结果输出。resp 是服务器返回的完整响应对象我们要的内容藏在 resp.choices[0].message.content 这个嵌套路径里这也是新手最容易搞错的地方坑四会细说。2.3 跑通之后先看懂这三层结构第一次跑通之后我建议不要急着往下做先把返回的响应理解透。大模型 API 返回的其实是一个结构化对象它的嵌套路径是有规律的。最外层是 resp表示一次完整的响应resp.choices 是答案列表正常情况下只有一个元素里面装着模型给你的回复choices[0].message 是模型消息对象里面包含 role 和 content 字段content 才是你要的文本。为什么搞这么多层嵌套因为一次 API 调用可以支持多个候选答案也支持返回一些附加信息比如 token 使用量、结束原因等。对新手来说只需要记住最终文本在 resp.choices[0].message.content 这个位置就行。如果还是不放心可以先加一行调试代码打印整个响应看一眼print(resp)这条路走通之后Agent 的基础桩就立住了。后面的所有高级功能无非是在这十行代码外面做文章。3. 踩坑实录四个坑逐个拆解代码虽然短但我实际跑的时候并没有这么顺。前前后后踩了不少坑最后筛出四个最典型的逐个说下现象、原因和解决办法。3.1 坑一密钥环境变量根本没载入先说这个最坑的。我第一次跑代码直接在终端里报错OpenAIError: The api_key client option must be set either by passing api_key to the client or by setting the OPENAI_API_KEY environment variable我当时很疑惑.env 文件明明放在项目根目录shell 里也设置了导出怎么还会报错后来排查发现问题出在 load_dotenv() 这行。python-dotenv 库并不是默认就帮你读取 .env 文件的你必须在代码里显式调用 load_dotenv() 才会加载。我第一次写代码时只安装了 openai没装 python-dotenv也没调用 load_dotenv()所以环境变量一直是空的。解决办法很简单安装 python-dotenv并在创建 client 之前调用 load_dotenv()。或者更直接的在终端里 export OPENAI_API_KEYsk-xxx然后再运行代码。不过这里我强烈建议用 .env 方案。把密钥写在一份独立文件里再把这个文件加进 .gitignore比直接在 shell 里 export 更安全也不会因为开新终端导致变量丢失。直接在代码里硬编码 api_key 是最不可取的万一代码传到公开仓库密钥就等于裸奔了。3.2 坑二messages 角色体系写错第二个坑是 messages 参数内部的 role 字段。Chat Completions 协议中 role 只能是 system、user、assistant 三种还有 tool 和 function那是工具调用时才用的但我看到过一些习惯性写法用 human 当 role直接照搬过来结果撞上 400 错误。报错信息很直接openai.BadRequestError: Error code: 400 - Invalid messages[0].role: must be one of [system, user, assistant, tool, function]除此之外还有一个更隐蔽的坑有人为了省事把 system prompt 直接塞进 user 消息里这虽然不报错但会让模型忽视系统指令得到的回复经常跑偏。比如你想让模型扮演客服系统指令里写了“只回答商品相关问题”结果用户问天气它也认真回答就是因为 system 指令没放对位置。正确用法是让三类角色各司其职system 放系统指令相当于给模型立规矩user 放用户的输入assistant 放模型的历史回复。多轮对话时每轮模型的回复都要以 assistant 角色追加到 messages 里否则模型无法理解对话上下文。这个设计其实是 Agent 框架里上下文管理最基础的一环后面做 Agent 时一直会用到。3.3 坑三max_tokens 设置太小回答被腰斩第三个坑不是报错是结果不对。我最初为了省 token给模型设了 max_tokens30然后让它写一段产品介绍结果回答到一半就停了像一个人话说到一半突然被捂住了嘴。一开始我以为是网络抖动反复重试了好几次结果还是老样子。后来随手打印出 finish_reason 字段才发现原因。正常结束的 finish_reason 是 stop表示“模型已经把回答完整输出完毕”而那次返回的是 length意思是“因为达到了 token 上限模型被迫停止生成”。解决方式有两个方向。一是调大 max_tokens比如 256 或 512具体值看任务复杂度一句话回复 128 足够一段详细分析至少 512 起步。二是改用流式输出把 streamTrue 加上模型生成一个字就返回一个字这样可以边生成边打印体感上速度快很多也更适合长文本场景。这里有个非常重要的经验凡是遇到输出不完整的情况先看响应里的 finish_reason。它能直接告诉你答案是正常结束、被截断还是触发了安全过滤省得你瞎猜半天。3.4 坑四响应解析路径出错对象当字典用第四个坑发生在解析返回结果时。我之前写习惯了 dict 风格的接口响应用 requests 库直接调 HTTP 接口时习惯性用字典索引来取值。后来换用官方 SDK发现结果类型变了代码直接崩了TypeError: ChatCompletion object is not subscriptable使用官方 SDK 时response 是一个对象不是字典正确的取值方式是点号属性访问resp.choices[0].message.content。但我还是习惯性写成了 resp[choices][0][message][content]自然报错。后来我改成了对象属性访问才顺利拿到结果。这里我学到的最重要一课是拿到一个新库的返回值先花几十秒把结构搞清楚再决定怎么取值。别凭经验猜直接打印出来看是最笨也最有效的方法。同时还要防一手 content 为 None 的情况当模型拒绝回答、命中安全过滤或者触发了某些特殊逻辑时content 可能是空值直接访问会得到 NoneType 相关的错误。防御做法是加一个判断content resp.choices[0].message.content if content: print(content) else: print(模型没有返回内容)4. 常见报错与排查速查表踩坑踩多了我总结了一张速查表遇到问题直接对照能省不少时间。这张表只覆盖入门最常遇到的五类情况但已经能解决大多数使用场景下的问题。报错/现象可能原因排查顺序OpenAIError: api_key must be set环境变量没加载检查 .env 是否存在、load_dotenv() 是否调用、shell 是否 exportHTTP 400: Invalid messages[0].rolerole 枚举写错检查 role 是否使用了 system/user/assistant输出到一半停住max_tokens 太小打印 finish_reason看是不是 lengthobject is not subscriptable把返回对象当字典改成点号属性访问content 为 None被过滤或未生成内容先打印整个 resp 看状态4.1 高频报错对照表除了上面的五类还有一些零碎问题也值得注意。比如 SSL 证书错误通常是系统时间异常或者本地 CA 证书不完整引起的和代码本身没关系再比如模型名写错会在请求阶段直接报模型不存在。这类问题的共同特点是一看报错信息就能定位关键是别慌先把英文报错翻译成人话大多数错误信息已经把答案写在脸上了。我的建议是把调试习惯养好第一次跑代码时可以先加一行 from pprint import pprint然后打印整个响应对象。虽然会刷屏但你能一眼看到返回的结构这对建立“模型返回长什么样”的直觉非常有帮助。后面一旦遇到结构变化你马上就能意识到哪里不对。4.2 我养成的四个防御习惯这四个习惯帮我避免了很多后面 Agent 开发中的大坑分享给你们第一API Key 永远走环境变量绝对不硬编码进代码里。一次性把 .env 加进 .gitignore防止误传仓库。第二拿到响应先看 finish_reason。这个字段会告诉你回答是正常结束、被截断还是触发了过滤。很多“回答不对”的问题一看这个字段就真相大白。第三用 SDK 就依靠 SDK 的对象结构不要自己猜字典。如果实在不确定返回结构就 print 整个响应。第四所有参数都先设保守值跑通再逐步放开。model 先选便宜的、参数先选稳妥的、max_tokens 先给够别一上来就追求复杂功能。5. 从“一次调用”到“真正的 Agent”还要补什么十行代码跑通后Agent 的最小地基已经立住了。但真正的 Agent 显然不只是“调用一次模型”它还差几个关键部件的拼装。5.1 循环让大模型自己决定下一步Agent 的核心是循环。单次调用只能答一个问题但在 Agent 场景里模型需要根据用户的最终目标和当前状态不断输出下一步动作再由代码执行并反馈给模型。最简单的循环就是多轮对话把所有历史消息都塞进 messages反复调用 create让模型在上下文里持续推进。下面是一个最简单的多轮对话循环骨架history [] while True: user_input input(你: ) history.append({role: user, content: user_input}) resp client.chat.completions.create(modelgpt-4o-mini, messageshistory) answer resp.choices[0].message.content print(Agent:, answer) history.append({role: assistant, content: answer})这个骨架其实就是 Agent 的雏形只是没有工具、没有规划、没有记忆管理而已。你跑一下就会发现模型已经能记住前面聊过什么上下文是连贯的了。5.2 工具调用Agent 的双手接下来要补的第二个能力是工具调用。光靠对话模型只能“想”不能“做”。想让 Agent 查天气、算数学或者访问数据库就需要函数调用机制开发者把可用的函数列表名称、描述、参数 JSON Schema传给模型模型在回答时先输出一个调用计划由代码执行该函数并把结果返回给模型模型再基于结果继续生成最终回答。第一次理解这个概念时我把它类比成“给模型装上了手和眼睛”。模型本身没有执行能力但代码可以替它执行执行结果再喂回给它这就形成了 Agent 的闭环。这一步是普通聊天机器人和 Agent 的分水岭。5.3 记忆管理上下文窗口是稀缺资源第三个要补的是记忆管理。上下文窗口是有限的资源对话一长历史消息就会把窗口撑爆。这时要么做滑动窗口只保留最近几轮要么定期总结历史把摘要塞回上下文再复杂一点还可以引入向量数据库做长期记忆检索。这个模块和前面相比不是一道简单的 API 调用而是要设计一套策略。实际开发中很多 Agent 跑着跑着就开始胡言乱语排查下来往往是上下文窗口被撑爆模型看到了过长的历史反而丢失了重点信息。所以记忆管理没做好Agent 就会“失忆”再强的模型也白搭。5.4 再往前走一步的方向再往后还有任务规划拆解、多 Agent 协作、人机交互流程设计等方向。但不管框架怎么变底层仍然是最初那十行代码的循环包裹。把这些串联起来看所谓的 Agent 开发本质上就是在“模型会说话”这个最小能力之上用代码不断给它加装备的过程。如果非要给后续学习排个优先级我的建议是先做工具调用再做记忆管理最后再碰多 Agent 协作。工具调用最容易出效果你给模型加一个天气查询函数它立刻就能回答“今天该不该带伞”这种需要实时信息的问题记忆管理影响的是长期体验多 Agent 协作更像是锦上添花等前面都顺手了再玩也不迟。最后聊一点我个人的感受。跑通这十行代码之后我再看各种框架文档突然就有了一种“原来如此”的通透感。框架再怎么封装底层就是一次次模型调用、角色消息栈的维护、工具函数的注册与执行这些在入门阶段手写一遍后面用来理解所有上层抽象都会轻松很多。所以我真心建议折腾 Agent 之前先别急着上框架用自己的代码把这条路走一遍。哪怕只有一次后面所有的调参、排错都会顺手不少。
返回列表