10行代码跑通大模型调用:Agent开发第一课与4个必踩的坑

📅 发布时间:2026/9/8 17:36:02
10行代码跑通大模型调用:Agent开发第一课与4个必踩的坑
1. 写在前面为什么我只用 10 行代码跑通第一版先交代一下背景。我最近在做一个 Agent 项目核心需求很简单让大模型能够根据用户输入自动调用工具、组织回复。但项目起步时我没想着一上来就上 LangChain、Semantic Kernel 这种重型框架而是打算先用最原始的方式——直接调用大模型接口把一次完整的对话流程打通再说。当时我把门槛卡得很死只用 10 行核心代码能跑通一个大模型接口调用拿到预期输出。倒不是为了炫技而是想验证一个判断——如果连最原始的接口调用都搞不清楚里面 token 怎么算、system prompt 怎么传、流式输出怎么接那后面做 Agent 的记忆、规划、工具调用全是空中楼阁。结果就是这么个“弱智”需求我愣是踩了 4 个坑有的坑是环境问题有的坑是参数设计问题有的坑纯粹是对接口协议理解不到位。前前后后折腾了小两个小时最后代码跑通的那一瞬间我反而觉得前面踩的坑比那 10 行代码本身更有价值。这篇不是教你写多复杂的 Agent 框架而是给同样打算从零开始做 Agent 的朋友一个“最小启动包”让你知道第一步究竟卡在哪里、为什么会卡、怎么绕过去。适用人群有基础编程能力、想入坑 AI Agent 开发但还没真正调通过大模型接口的开发者。已经能熟练跑通接口的老手可以直接跳到踩坑实录部分。先说结论10 行代码跑通大模型调用绝对可行但前提是你得先解决 Python 环境、密钥配置、HTTP 请求格式、返回结果解析这四件事。这四件事任何一个出问题都会让 10 行代码变成 100 行排查。下面我把完整过程展开讲。2. 先搞清楚一个大前提Agent 和“一次大模型调用”之间的关系正式开始写代码前我得先把 Agent 这个概念拆一下。现在网上聊 Agent 的文章太容易把人绕晕了——什么规划、记忆、工具调用、多智能体协作、AutoGPT 跑起来的炫酷 demo……但说到底任何一个 Agent 系统落到最底层一定会发生无数次“给大模型发请求、拿到回复、决定下一步做什么”的循环。我习惯把 Agent 的核心里面拆成三个层次接口层负责跟大模型服务端通信处理 HTTP 请求、鉴权、流式与非流式响应、token 计数。这一层是地基。逻辑层负责“思考”——根据用户目标拆解任务决定调哪个工具、传什么参数、怎么评估结果。工具层真正执行动作的地方比如搜索、计算、读写文件、调外部 API。绝大多数新手犯的错误是一上来就研究逻辑层和工具层然后被各种抽象概念困住。但逻辑层里所谓的“思考”本质上就是精心设计 prompt 然后丢给大模型工具层里的“调用”本质上就是根据大模型返回的一段 JSON 去执行对应函数。两层全都依赖接口层跑通。所以“用 10 行代码跑通第一次大模型调用”这个事在我眼里就是 Agent 开发的第一个里程碑。它验证的不是你有多会写代码而是你有没有能力让大模型成为你程序的一部分——让外部世界的自然语言输入变成你代码里一个可用的字符串变量。我这次选的是 Python原因很简单生态最成熟requests 库三行能解决的问题别的语言可能要写十行还伴随各种坑。Node.js 我也试过后面踩坑实录里会提到对比结论。3. 10 行核心代码逐行拆解我到底写了什么先贴我自己实际跑通的版本Python requests不依赖 OpenAI SDK每一行后面我会讲清楚它在整个 Agent 链路里扮演什么角色import requests import os import json api_key os.environ[LLM_API_KEY] url https://api.example.com/v1/chat/completions response requests.post( url, headers{Authorization: fBearer {api_key}, Content-Type: application/json}, json{ model: gpt-4o-mini, messages: [ {role: system, content: You are a helpful assistant.}, {role: user, content: 用一句话解释什么是Agent} ], temperature: 0.2 }, timeout30 ) print(response.json()[choices][0][message][content])数一下核心代码确实只有 10 行左右不算空行和 print 前面的缩进。但这 10 行不是一拍脑袋写出来的它背后涉及好几个关键设计决策3.1 为什么用 requests 而不用 OpenAI SDK市面上几乎所有主流大模型厂商都会提供官方 SDK比如 openai-python、anthropic-sdk用起来确实更简洁。但我故意绕开了 SDK坚持用 requests 裸调接口。原因有两条第一SDK 容易让人忽略协议细节。Agent 开发越往后走你越需要理解 HTTP 状态码、流式响应格式、错误体结构这些底层逻辑。用 requests 裸调你会被迫面对返回的每一个字段理解 messages 数组为什么是那样组织、token 消耗在哪里查、模型报错时应该看哪个字段。这些理解在后续做 Agent 框架、调试工具调用时全是硬通货。第二不同厂商的接口兼容性。很多大模型服务商都提供 OpenAI 兼容接口但细节总有差异。用 requests 自己封装一层供应商切换时只需要改 URL 和参数映射比绑死在某个 SDK 上灵活得多。3.2 为什么必须是“数组形式的 messages”接口里最关键的是messages参数。它不是简单的字符串拼接而是一个数组数组里每个元素都有role和content两个字段。这个设计是 Chat Completion 协议的核心也是 Agent 系统所有“记忆”机制的底层基础。system角色给大模型设定人设、边界、行为规则。对应到 Agent 里就是系统提示词System Prompt它决定了 Agent 的“人格”。后续你让 Agent“扮演客服”“扮演代码审查员”就是改这里的几行字。user角色真实的用户输入。对应 Agent 应用里就是用户每次发的那句话。assistant角色大模型自己的历史回复。多轮对话时需要把之前的用户输入和模型输出都回传给接口模型才能“记住”上下文。Agent 的记忆机制说白了就是在不断操作这个数组。我把这个设计叫“上下文冰箱”messages 数组就像一个冰箱你把对话历史一份份冻在里面每次请求时全端出来给模型看。要不要放某样东西、放多少全看你怎么管理这个数组。后面做 Agent 的“记忆压缩”“长期记忆”本质上都是在处理 messages 数组的大小和结构。3.3 temperature 参数为什么设 0.2Agent 场景下我强烈建议把 temperature 设低0.1-0.3之间。这个参数控制的是模型输出的随机性——值越高回答越发散、越有创造性值越低回答越保守、越确定。Agent 不是聊天机器人它需要稳定地输出结构化结果然后程序才能根据结果执行下一步。如果 temperature 设成 0.9模型可能这次返回一个 JSON 格式、下次就乱加一段说明文字你的解析代码就崩了。做 Agent稳定大于惊喜。提示如果你发现在调接口时返回的内容格式不稳定先检查 temperature再说 prompt。3.4response.json()[choices][0][message][content]这串取值是什么意思第一次跑的时候我看返回结果也觉得奇怪为什么大模型接口返回的不是一个干净的字符串而是包了一层又一层。这个结构拆开看是这样的{ id: chatcmpl-xxx, object: chat.completion, created: 1700000000, model: gpt-4o-mini, choices: [ { index: 0, message: { role: assistant, content: 这是模型真正说的话 }, finish_reason: stop } ], usage: { prompt_tokens: 30, completion_tokens: 20, total_tokens: 50 } }设计成这么深的层级不是故意刁难你。choices是数组意味着接口支持一次性返回多个候选回复比如做A/B测试时可以请求返回2-3个不同结果message里的role表示这段回复的角色finish_reason告诉程序这次生成是因为说完了stop还是触发了最大长度限制length。这些字段在 Agent 开发里全都有用——比如判断 Agent 是不是因为输出太长被截断了。所以别看这行取值代码啰嗦它其实在教你一个道理大模型接口返回的是一个结构化对象不是一个字符串。你在程序里拿到这个对象后可以做判断、做分支、做解析这才是 Agent 逻辑层能跑起来的前提。4. 4 个坑逐个说清楚踩坑过程完整还原这部分是我最想分享的。前面说了跑通本身不难难的是你不知道会在哪个环节被卡住然后开始怀疑人生。我踩的 4 个坑按时间顺序排每个都有完整的排错思路你能复现。4.1 坑一环境变量读取失败密钥根本没传进去我刚开始写代码时图省事直接把自己申请的 API 密钥明文写在代码里像这样api_key sk-xxxxxxxxxxxxxxxx第一版跑通了然后我得意地把代码发到群里被一个朋友提醒这东西要泄露了你的余额就没了。于是改成用环境变量api_key os.environ[LLM_API_KEY]改完后直接运行报错KeyError: LLM_API_KEY我当时第一反应是“这行代码写错了”排查了五分钟之后才意识到是环境变量压根没配置。在命令行里设置环境变量的方式是这样的# Linux / Mac export LLM_API_KEYsk-xxxxxxxx # Windows PowerShell $env:LLM_API_KEYsk-xxxxxxxx但问题又来了你在这个终端窗口设置了环境变量换一个终端窗口运行 Python 脚本还是读不到。这是因为环境变量只在当前进程及其子进程中生效不会全局永久保存。你关掉终端这个变量就没了。我当时踩得更深一步用 Visual Studio Code 打开项目配置了 launch.json里面设置了环境变量但运行后照样 KeyError。查了半天发现是我改了 launch.json 之后没有重启 VS Code 的 Python 调试进程环境变量没有重新加载。注意修改环境变量之后一定要重启终端、重启 IDE 进程否则系统不会自动读取新的环境变量。最后的解决方式很朴素——在项目根目录建一个.env文件然后用python-dotenv库加载pip install python-dotenvfrom dotenv import load_dotenv load_dotenv()把密钥统一放在.env里配合.gitignore把该文件排除掉这才是工程化的做法。密钥泄露是 Agent 项目里面最不能犯的错误之一因为你调大模型接口是花的真金白银。4.2 坑二返回 401 认证失败Authorization 请求头拼错了环境变量搞定后代码能跑起来了但紧接着弹出来一个 401{error: {message: Incorrect API key provided, type: invalid_request_error}}这个报错的意思是服务端收到你请求了但没认出你的身份。我当时第一反应是“密钥过期了”准备去后台重新生成一个。但转念一想我昨天刚申请的啊怎么就能过期。于是我去翻自己的请求头发现了一个低级的拼写错误——我在 Python 里用双引号包字符串时把Authorization写成了Authorizationn中间多打了一个字母 n。这个错误有多隐蔽呢你肉眼盯着代码看未必能看出来因为变量名又不是保留字不报 NameError只是 header 里多了一个没有意义的键。服务端收到请求时一看 header 里没有Authorization字段自然就把你当成了匿名用户返回 401。这个坑给到我的教训是HTTP 请求头是键值对匹配键名写错是不报错的只会以认证失败的形式出现。排查的时候不能只看 Python 代码本身还要看实际发出的 HTTP 请求长什么样。我的经验是在requests.post(...)之前先打印一下 headersprint(headers) print(api_key)确保打印出来的内容里密钥真实存在、头部字段名没有拼错。虽然这不算一个“高级技巧”但90%的认证问题靠这一招就能解决。4.3 坑三请求超时想当然模型没在 10 秒内“想好”就断了认证通过之后请求发出去服务端开始处理浏览器里的小圈圈转啊转。我当时天真地把timeout设成了 10 秒心想大模型回答一句话能有多慢。然后我就撞上了requests.exceptions.ReadTimeout。这个报错很枯燥但背后的逻辑值得讲清楚requests库的timeout参数不是一整个请求的总时间而是连接阶段和读取阶段各自的最长等待时间。大模型生成 token 是流式的、逐步的一个问题可能要生成几十个 token即使每个 token 只需要零点几秒加起来也可能超过 10 秒。所以在 Agent 开发里timeout 的取值需要根据任务复杂度动态调整任务类型建议timeout简单问答一句话回复10-15秒代码生成 / 长文本输出30-60秒复杂多步推理60-120秒甚至更长我把 timeout 改成了 30 秒之后跑通了。但这也引发了我另一个思考——大模型不是无限等待服务的它也有反应上限。如果你让 Agent 执行的任务太复杂模型的响应时间会拉长用户体验变差的同时你的服务成本也在上升。这就是为什么真正做 Agent 时一定要对任务做拆解把大任务拆成小步骤每个步骤单独调一次模型而不是让模型一口气“想完”。一方面响应更快另一方面出错后也更容易定位。4.4 坑四输出被截断finish_reason不是 stop好不容易 200 返回了代码也不飘红了但我发现一个诡异的现象让模型写一段 300 字的小总结它只写到 200 字就突然刹车末尾多了一个省略号一样的中断感。我一开始以为是温度太高导致模型开小差把 temperature 从 0.7 降到 0.2 之后问题依旧存在。然后我准备骂服务商了心想这是什么垃圾模型连 300 字都憋不出来。结果我翻了一下返回的 JSON发现choices[0][finish_reason]的值是length不是stop。也就是说回复不是因为模型觉得说完了才停下来的而是因为达到了最大 token 数限制被强制截停了。问题不在模型在我自己。我在接口调用时设置了一个max_tokens限制虽然我有意控制输出长度但因为文字是中文一个汉字通常要占 1-2 个 token而我没仔细算只按英文字符的量去估计了。低代码水平下 Token 预估错误是实习生最容易踩的坑。正确的做法是在系统提示词里告诉模型你期望的输出长度同时在接口参数里给足 max_tokens 余量并在代码里检查finish_reasonfinish_reason response.json()[choices][0][finish_reason] if finish_reason length: print(提示回复被截断可能需要增大 max_tokens或优化 prompt 让模型更简洁)这个机制对 Agent 开发尤其重要。你让 Agent 生成一份 JSON 作为工具调用的参数结果 JSON 被截断了一半你拿什么去解析所以我在项目里统一做了一个后置校验如果 finish_reason 是 length就丢弃这轮回复重试一次或者提示用户输入更精确的问题。5. 跑通之后再往前走一步把 10 行代码扩展成“最简 Agent 雏形”10 行代码跑通的那一刻说实话成就感是有的但也就持续了几分钟。因为我也知道这离真正的 Agent 还差着十万八千里。但关键是你已经验证了最重要的一个事实大模型接口已经可以被你的程序调用你现在有了“大脑”接下来该给它装“手”和“脚”了。5.1 加上“取当前时间”的工具调用逻辑我的 Agent 雏形第一版只做了一件事让大模型判断用户输入里是否包含“时间”这个意图如果包含就调用一个本地函数返回当前时间然后把时间拼进回复里。这个逻辑在框架里叫“工具调用”Function Calling但你其实可以不用任何框架纯手写也就几十行。核心套路是三步把可用的工具列表告诉大模型描述工具名称、参数、用途。让大模型决定是否调用工具、怎么传参返回一个结构化结果。程序拿到结构化结果后执行对应函数再把执行结果以tool角色的消息回传给大模型让大模型整合成最终回复。这一步跑通之后你对 Agent 的“工作原理”就有了肌肉记忆Agent 不是让大模型替你做所有事而是让大模型学会“发号施令”你用自己的代码去执行命令。想清楚这一点你再去看 AutoGPT、BabyAGI 这些项目时就会发现他们的核心也没那么神秘都是围绕“模型决定 代码执行 结果回传”的循环在转。5.2 加一个最小记忆机制在雏形版本里我还给消息数组加了一个“轻量记忆”功能。办法很笨但有用每次用户发消息时把 history 数组里最近的 5 轮对话打包发给模型。history [] while True: user_input input(你) history.append({role: user, content: user_input}) payload { model: gpt-4o-mini, messages: [{role: system, content: 你是我的Agent助手}] history[-10:], } # 调用接口... response ... history.append({role: assistant, content: response})这个机制虽然简单但它验证了一个核心观点大模型的“记忆”本质上是上下文窗口的搬运工。你把多少东西塞进请求里它就能“记住”多少东西。真正的 Agent 项目里那些花里胡哨的长期记忆、向量检索、记忆压缩方案都是在解决“怎样才能在有限的上下文窗口里塞进最有价值的历史信息”这个问题。你现在手动拼一个数组以后换向量数据库思路是一样的。5.3 加入简单的错误重试机制调用大模型接口不可能是 100% 成功的。网络抖动、限流、模型服务过载、返回内容格式非法……这些异常在真实环境中都会出现。Agent 不是只在理想环境里跑一次的脚本它要稳定服务必须有容错机制。我在雏形版里加了一个非常朴素的重试逻辑def call_llm(messages, retries3): for i in range(retries): try: response requests.post(...) response.raise_for_status() return response.json() except Exception as e: print(f第{i1}次调用失败{e}) time.sleep(2 ** i) # 指数退避 raise RuntimeError(多次调用后仍然失败)time.sleep(2 ** i)就是指数退避的核心——第一次失败等 2 秒第二次等 4 秒第三次等 8 秒。好处是给服务端留出恢复时间也避免在限流状态下疯狂重试导致被封。别小看这几行代码Agent 进程一旦跑长所有你想象不到的异常都会冒出来重试机制是最基础的保命符。6. 一些关于选型和方向的补充建议跑通最小闭环之后接下来面临的问题往往是“我要不要上框架”。我以前也是重度框架使用者但这次从零手撸经历让我改变了想法。6.1 LangChain 这类框架什么时候上、什么时候别上我的建议是在你还没亲手调通过大模型接口之前先别上框架。因为这个阶段你连基础协议都没吃透遇到框架抛出的抽象概念会一头雾水。框架的封装固然帮你省了时间但也把很多关键细节藏了起来——你只知道“给 Chain 加一个 Memory”却不知道它在底层不过是在维护一个 messages 数组。万一出了问题你连怎么排查都不知道。等到你能熟练地裸调接口、理解流式输出、会设计工具调用 JSON 结构再上 LangChain 就会发现那些概念全都有对应的底层实现你只是换了一套更高层的 API 而已这时候框架才真正帮你省力。6.2 Python 和 Node.js 怎么选现在很多做前端的朋友也在尝试写 Agent经常纠结该用 Python 还是 Node.js。我的实际体验是Python 做原型验证最快requests 简洁pandas、numpy 处理后端数据也方便。但部署稍微麻烦一点依赖管理也让人头疼。Node.js 胜在异步非阻塞如果 Agent 涉及大量并发的 HTTP 请求比如同时调用多个工具Node.js 用 Promise.all 可以更优雅。而且前端团队的技能栈直接复用。我的建议是别纠结选你熟的那个。Agent 的核心逻辑框架跟语言无关你踩过的那些接口调用坑换一门语言照样会遇到反而因为对语言不熟会增加调试难度。先跑通再优化这个顺序不能反。6.3 从“调接口”到“Agent 项目”的路径参考最后给一个我从这次经历中总结出来的学习路径参考给同样从零起步的朋友阶段一裸调接口跑通一次非流式对话。完成本文这些内容理解 messages、token、temperature。阶段二实现流式输出。让模型一个字一个字蹦出来这对实时交互体验至关重要。核心是把stream参数设为 true然后逐行解析 SSEServer-Sent Events数据。阶段三实现多轮对话 记忆管理。手动维护 messages 数组实现滑动窗口记忆。阶段四实现 Function Calling工具调用。让模型学会返回 JSON 指令你实现函数执行器。阶段五尝试让 Agent 自主拆分任务。做一个简单的主循环由模型决定下一步行动你只负责执行。这套路径走完你已经可以自己写一个雏形 Agent 框架了。回头再看那些开源 Agent 项目观感会完全不一样——你不再是被动地看别人的抽象概念而是能一眼看出某个模块是在实现哪一层能力。7. 总结一下我自己在这件事上的感受把整个经历写出来之后我自己又复盘了一遍。其实“10 行代码跑通大模型调用”这个目标本身并不难难的是过程中遇到的那些错误什么时候出现、为什么出现。AI 这个领域现在更新太快网上的教程一搜一大把但大多是顺风顺水的成功路径展示没人告诉你第一次调接口会撞上多少个莫名其妙的报错。而我想说的是这些报错恰恰是最有价值的教材。401 教会我检查 HTTP 请求头ReadTimeout 教会我估计模型的响应时间finish_reason 教会我看官方文档而不是骂服务商环境变量问题教会我工程化是写代码的一部分。这四件事是任何“5分钟跑通大模型调用”的短视频都不会讲给你的但它们决定了你在 Agent 开发这条路上能走多远。最后再分享一个小技巧。我的项目现在每次启动都会先跑一个“连通性自检”——用最简单的 system prompt 调一次接口看看返回是否正常、延迟多大、 token 消耗多少。这个方法帮我在写新功能前提前发现环境问题也让我对项目的“健康状态”心里有数。做 Agent 项目千万别等整个流程跑起来才发现接口挂了那排查起来才是真折磨。