拓冰建站拓冰建站
首页 / 资讯中心 / 正文

手撸大模型API调用:10行代码跑通,Agent底层循环与踩坑全复盘

1. 为什么我坚持“手撸”而不是直接用 LangChain先说个背景。我接触 AI Agent 这个概念有半年多了但一直处于“看文章很懂、动手就废”的状态。市面上的教程分两种一种是讲概念讲得天花乱坠什么规划、记忆、工具调用、多智能体协作听完感觉脑子会了另一种是直接甩一个 LangChain 或 MetaGPT 的 Demo让你pip install然后跑起来。但说实话跑了半天你连“这个 Agent 到底是怎么回答出这句话的”都说不清楚一旦报错更是两眼一抹黑。所以这次我给自己定了个规矩不装任何 Agent 框架不用任何封装好的 SDK只靠最原始的 HTTP 请求库从零写代码调用大模型 API。目标就一个让大模型在终端里回答我一句话然后基于这句话理解 Agent 背后那个最基本的“模型调用循环”长什么样。为什么非得这么干因为 Agent 再复杂底层的原子操作都是同一个把一段对话历史发给大模型拿到返回的文本然后判断要不要继续调用工具。你先把这个原子操作吃透了后面上 LangChain、AutoGen、或者自己设计多智能体架构都是水到渠成的事。反过来如果你一上来就拉着框架跑遇到问题根本不知道是框架的问题、模型的问题还是你 Prompt 的问题。这篇文章我要分享的是我是怎么用 10 行核心代码跑通第一次大模型调用的以及在这个过程中踩到的 4 个坑。每个坑我都会把完整排查链路写出来包括当时的报错信息、我的错误猜测、以及最终的根因。你会发现绝大多数坑都不是什么高深的技术问题而是对“API 边界”理解不到位。先说清楚选型。语言我选了 Python因为它做文本处理、测试脚本都方便。HTTP 请求库用的是requests不用 OpenAI 官方 SDK。原因很简单——官方 SDK 帮你隐藏了太多细节我要的就是“裸调”的感觉这样你才能看到请求体的真实结构。模型我选了 DeepSeek 的 API因为注册就送额度不需要绑卡对新手极其友好。当然这篇文章的思路对 OpenAI、通义、文心、Kimi 等任何 OpenAI 兼容格式的接口都适用只需要改base_url和model两个参数。提示如果你之前完全没写过代码建议先花半小时熟悉一下 Python 的基础语法至少要能看懂import、print、函数定义。如果你已经能独立写百行左右的小脚本那这篇教程你照着敲一遍半小时内就能跑通。2. 环境准备最容易忽略的 3 个细节网上大多数教程对环境准备都是一笔带过导致新手在第一步就卡住。我自己是重装了一台新电脑之后才意识到环境这关有 3 个细节特别容易踩。2.1 Python 虚拟环境别把依赖装进全局很多人拿到 Python 第一件事就是pip install requests直接装到全局环境里。这样做短期内没问题但你的项目一多、依赖一乱各种“版本冲突”就来了。我这次用的是 Python 3.10 venv 虚拟环境。# 创建虚拟环境 python -m venv agent_env # 激活虚拟环境Mac/Linux source agent_env/bin/activate # 激活虚拟环境Windows # agent_env\Scripts\activate # 安装依赖 pip install requests虚拟环境的好处是你在.venv里装任何东西都不会污染全局环境删掉项目文件夹就等于卸载干净了不用纠结“我装过什么”“要不要卸载”。2.2 API Key 的存放打死别硬编码在代码里我看过太多教程在代码里直接写api_key sk-xxxx。这习惯非常差——代码一旦传到 GitHub 上Key 就泄露了别人拿着你的 Key 疯狂调用是要烧你钱包的。正确做法是用环境变量或者.env文件。我这里用.env文件加python-dotenv库pip install python-dotenv项目根目录创建一个.env文件把 Key 放进去DEEPSEEK_API_KEYsk-你的密钥然后 Python 代码里这样加载import os from dotenv import load_dotenv load_dotenv() api_key os.getenv(DEEPSEEK_API_KEY)2.3 代理与网络环境的坑这一步是我“踩坑预演”中提前发现的如果你的电脑开了系统代理比如公司网络、或者本地调试用的代理工具requests默认会走系统代理容易导致请求失败或超时。如果你确认自己的网络能直连 API 域名可以显式关闭代理proxies {http: None, https: None} response requests.post(url, headersheaders, jsonpayload, proxiesproxies, timeout30)如果没开代理却超时那就反过来检查是不是公司防火墙拦截了域名可能需要配置内部代理地址。这属于网络环境问题不同情况的解法正好相反特此提一下。注意API Key 是一个 32 位以上的随机字符串本质是你的身份凭证。别截图发给别人别提交到 Git 仓库不然等着你的就是“账单爆炸”。3. 10 行核心代码逐行拆解一个完整的模型调用很多教程喜欢一上来就贴 50 行代码看得人头大。我这次刻意压缩把核心调用逻辑压缩到 10 行以内再加点辅助代码也总不超过 30 行。先贴完整代码然后逐行讲。import os import requests from dotenv import load_dotenv load_dotenv() api_key os.getenv(DEEPSEEK_API_KEY) url https://api.deepseek.com/chat/completions headers {Authorization: fBearer {api_key}} payload { model: deepseek-chat, messages: [ {role: system, content: 你是一个乐于助人的AI助手}, {role: user, content: 你好请用一句话介绍你自己} ] } resp requests.post(url, headersheaders, jsonpayload, timeout30) data resp.json() print(data[choices][0][message][content])核心就是 8-19 行掐头去尾中间那 10 行就是整个调用的主干。我们来逐块拆解。3.1 请求地址和请求头API 的门牌号url https://api.deepseek.com/chat/completions这是 API 的入口地址。注意这个地址由两部分组成https://api.deepseek.com是 Base URL/chat/completions是具体的接口路径。如果你用的 OpenAI 官方Base URL 是https://api.openai.com/v1路径同样是/chat/completions。如果用通义千问Base URL 又不一样。但好消息是只要接口兼容 OpenAI 格式你的代码结构就是一模一样的。headers {Authorization: fBearer {api_key}}这是认证信息。Bearer是一种令牌认证方式意思是“持有此令牌的人有权访问该资源”。你把 API Key 放在这里服务端验明正身才会响应你的请求。很多人第一次用requests不习惯这种格式容易直接传Authorization: api_key那必定报 401。3.2 请求体大模型真正的“输入”payload { model: deepseek-chat, messages: [ {role: system, content: 你是一个乐于助人的AI助手}, {role: user, content: 你好请用一句话介绍你自己} ] }这是整个请求中最核心的部分。注意messages是一个列表里面每个元素是一个“消息对象”包含两个字段role和content。role取三个值system系统指令、user用户输入、assistant模型回复。system消息是可选的但你一旦加了它它就会像一个“性格底色”一样影响模型的整体输出风格。user是你当前想问的问题。assistant通常用来传多轮对话的历史回复。这里有个新手常有的误区以为大模型有记忆。错了大模型是“一次性”的——它没有记忆它只根据你这次请求里messages列表中的内容来生成回复。你想让它记住之前的对话就得把之前的用户消息和模型消息都放进这个列表里。这就是为什么 Agent 框架里都会做一个“消息历史管理”模块。3.3 发送请求与解析响应resp requests.post(url, headersheaders, jsonpayload, timeout30) data resp.json() print(data[choices][0][message][content])这 3 行做了三件事发送 POST 请求、把响应转成 JSON、取出模型回复的文本内容。其中resp.json()返回的是一个嵌套字典结构长这样{ id: chatcmpl-xxx, choices: [ { index: 0, message: { role: assistant, content: 你好我是DeepSeek一个由深度求索公司开发的AI助手... }, finish_reason: stop } ], usage: { prompt_tokens: 15, completion_tokens: 25, total_tokens: 40 } }choices是一个列表里面每个元素代表一个候选回复。大多数情况下你只需要取choices[0]。finish_reason字段值得关注它告诉模型是因为什么停止生成——stop表示自然结束length表示因为超出最大长度被截断了tool_calls表示模型请求调用工具这在 Agent 开发中很关键。跑成功之后终端会输出模型自我介绍的一段话。看到这个输出你的第一次大模型调用就算跑通了。4. 实测踩坑全程复盘我踩的 4 个坑及完整排查链路这部分是本文的核心。我不打算只把报错和解决方案列出来那样你遇到变种问题还是不会。我会还原我的完整排查思路包括过程中的错误猜测和测试方法。4.1 坑一ModuleNotFoundError—— 虚拟环境装错位置报错信息ModuleNotFoundError: No module named requests我的第一反应装啊pip install requests就完事了。结果报错依然存在。排查过程我检查了pip list发现 requests 明明已经装了。这就奇怪了装了模块却提示找不到。后来我意识到可能是我激活的虚拟环境和安装依赖时的环境不一致。输入which python和which pip发现两个指向的不是同一个 Python。安装时用的是系统 Python 的 pip运行时用的却是虚拟环境里的 Python。相当于你在一栋楼的 3 楼装了快递柜却在 1 楼找包裹。根因我先运行了pip install requests然后才python -m venv agent_env创建虚拟环境并激活。安装动作发生在虚拟环境创建之前所以装到了全局环境。解决方案先创建并激活虚拟环境再执行 pip 安装。或者干脆在装完依赖后重新创建虚拟环境。正确顺序是python -m venv agent_env source agent_env/bin/activate pip install requests python-dotenv这里也建议你每次安装依赖前先看一下which pip的路径里有没有带着你当前虚拟环境的名字眼花了就确认一下。4.2 坑二401 Authentication Error—— 环境变量没加载成功报错信息{ error: { message: Authentication Fails, please check your API Key, type: authentication_error } }我的第一反应难道是 API Key 写错了我复制粘贴检查了两遍确认没错。再看报错问题不大但也不小我打印了一下api_key变量发现它输出的是None。这时候才意识到load_dotenv()没把.env文件里的变量加载进来。进一步排查我检查了.env文件的位置发现它放在了src子目录下而代码在项目根目录运行所以load_dotenv()默认找的是当前工作目录下的.env当然找不到。这就好办了。解决方案有两种方法一指定路径加载.env文件。load_dotenv(os.path.join(os.path.dirname(__file__), .env))方法二把.env文件放到项目的根目录确保工作目录正确。我更推荐方法二因为项目根目录放.env是通用惯例。额外提醒有些同学用 PyCharm 自带的 Run 功能工作目录会变成配置里的“Working Directory”这就导致.env文件明明就在项目根目录依然加载失败。遇到 401 先print(api_key)看是不是None能省掉大量瞎猜时间。4.3 坑三AttributeError: dict object has no attribute content—— 我没有真正拿到 OpenAI 官方 SDK 那样的对象报错信息AttributeError: dict object has no attribute content我的第一反应这行我看不懂感觉像是返回的数据结构不对。排查过程这坑其实是我临时调整导致的——我一开始还写了个调用函数顺手想用官方 SDK 风格的.content访问内容。但问题是requests库返回的resp.json()本来就是一个普通的字典不是 OpenAI SDK 返回的 Response 对象。官方 SDK 封装了response.choices[0].message.content这种属性访问方式但裸调用requests只有data[choices][0][message][content]这种方式。我去访问.content当然找不到因为字典没有这个属性。如果你用的是requests就用字典下标方式如果你用 OpenAI SDK才可以用属性访问。二者机制完全不同要区分。小技巧遇见这类“对象没有某个属性”的报错先print(type(变量名))看类型再print(变量名)看结构基本就能定位。4.4 坑四ConnectionError—— 各种网络连接问题代理和域名解析报错信息requests.exceptions.ConnectionError: HTTPSConnectionPool(hostapi.deepseek.com, port443): Max retries exceeded with url: /chat/completions (Caused by NewConnectionError(urllib3.connection.HTTPSConnection object at ...: Failed to establish a new connection: [Errno 8] nodename nor servname provided, or not known))我的第一反应我是不是没连网这就有点上头了——我明明浏览器能打开网页。排查过程我ping api.deepseek.com发现 ping 不通域名解析失败。但我用浏览器又能打开。后来才明白浏览器和requests的网络路径不一样。浏览器可能自动走了系统代理或者用了浏览器的专用网络栈而requests默认会读取系统的环境变量代理在某些配置下反而出问题。这里有一个很重要的区分如果你在代码里没显式设置代理requests会去环境变量里找HTTP_PROXY、HTTPS_PROXY找到了就用找不到就直连。我这次的情况是系统环境变量里有一个失效的代理地址导致requests试图通过这个代理访问但代理本身已经挂了。解决方案既然明确了是代理问题就分两步走。第一步“去掉环境变量里的代理”或者设置NO_PROXY第二步在代码里显式传入proxies{http: None, https: None}强制直连。resp requests.post(url, headersheaders, jsonpayload, proxies{http: None, https: None}, timeout30)如果你在公司网络或某些需要认证才能上网的环境下可能需要反过来设置正确的代理地址和认证信息才能访问外网。这个要看具体网络环境来判断。4.5 踩坑小结四个坑看似各不相干其实都是“环境问题”的变体环境装错、环境变量没加载、返回结构误解、网络环境代理。这类问题最大的特点是代码本身没错但环境和预期不一致。这类问题处理多了你会形成肌肉记忆——先确认环境再怀疑代码。排查时看报错信息别慌一步步打印变量、确认状态。5. 从 10 行代码到 Agent模型调用循环的真面目跑通 10 行代码只是一个起点。接下来你需要理解一个关键概念大模型本身不是 AgentAgent 是在大模型外面套了一层“循环逻辑”。5.1 普通对话模式 vs Agent 模式普通对话模式是“一问一答”用户提问模型回答结束。你刚才跑通的代码就是这个模式。Agent 模式则是在模型回答之后增加了一个“判断”环节把用户问题和其他上下文发给模型。模型回复一段文本或者一个“工具调用指令”。程序解析模型的回复判断它是想直接回答还是想调用某个工具。如果是调用工具程序执行工具比如查天气、算数学、搜资料把结果拼接进消息历史。再次把新消息发给模型让模型基于工具结果给出最终回答。这个“判断-执行-反馈-再回答”的循环就是 Agent 最核心的机制业界通常叫 ReAct 模式Reasoning Acting。当你看到 LangChain 的AgentExecutor、AutoGen 的ConversableAgent它们内部做的事情本质上就是上面这个循环的工程化封装。5.2 怎么让模型“请求调用工具”要让模型主动请求调用工具需要在请求体里添加tools参数。以 OpenAI 兼容接口为例{ model: deepseek-chat, messages: [ {role: system, content: 你是一个智能助手当需要计算数学题时请调用计算工具。}, {role: user, content: 123乘以456等于多少} ], tools: [ { type: function, function: { name: calculate, description: 计算两个数字的数学运算结果, parameters: { type: object, properties: { expr: { type: string, description: 数学表达式 } }, required: [expr] } } } ], tool_choice: auto }当你加了tools参数后模型返回的数据里可能出现finish_reason: tool_calls并且在message里多出一个tool_calls字段里面包含要调用的函数名和参数。你的程序解析这个字段执行对应的函数再把结果以role: tool的消息发回给模型模型才会整理成自然语言回答。这也是为什么 10 行代码跑通很重要——你要先把基础的“文本请求/文本响应”链路弄明白再去猜“工具调用”的那一层就不容易糊了。5.3 消息历史与记忆Agent 的记忆其实就是“每次全量发”前面我说过大模型没有记忆。不少同学不理解“为什么 ChatGPT 网页版能记住我上句话”因为它把整个对话历史都存着每次请求把所有消息一起发过去。ChatGPT 网页版后端帮你做了消息历史管理并不是模型自己记住了。这一点在自研 Agent 时尤其重要。你需要自己维护一个messages列表每次请求前把之前的历史都塞进去messages [ {role: system, content: system_prompt} ] def chat(user_input): messages.append({role: user, content: user_input}) resp requests.post(url, headersheaders, json{ model: deepseek-chat, messages: messages }, timeout30) assistant_reply resp.json()[choices][0][message][content] messages.append({role: assistant, content: assistant_reply}) return assistant_reply但你会很快遇到一个新问题上下文长度有限制。模型能接受的 token 总量是有限度的历史对话一长就会超出限制。这时候就需要做“精简历史”的策略比如保留最近 N 轮、对早期对话做摘要。这些都是 Agent 开发中的实际问题没有统一的正确答案得根据你的场景去取舍。6. 常见错误速查表以后踩坑对着查为了让你少走弯路我把这次过程中遇到的、以及身边朋友常遇到的问题汇总成一张表格。收藏这一张就能覆盖一大半新手期报错。报错信息可能原因排查方法解决方案ModuleNotFoundError: No module named requests虚拟环境未激活、安装到其他环境which python、which pip确认 pip 和 python 同环境激活虚拟环境后重新 pip install401 Authentication ErrorAPI Key 无效、环境变量未加载print(api_key)看是否为None重新配置.env文件或核对 Key404 Not Found请求地址或路径错误检查 Base URL 是否带/v1按官方文档核对接口地址Invalid URL请求 URL 拼错打印 url 变量检查是否有中文字符、多余空格AttributeError: dict object has no attribute xxx把 requests 响应当成了 SDK 对象print(type(data))、print(data)用字典下标方式访问字段ConnectionError: Max retries exceeded网络不通、代理失效、域名解析失败ping域名、检查系统代理显式传proxies参数或检查网络Timeout请求超时延长 timeout 参数设置timeout60或更长ContextLengthExceeded发送的 token 超过模型上限计算消息总 token精简消息历史、缩短输入RateLimitError请求频率过高或余额不足查看响应头中的限流信息加 sleep 延时或提高额度注意看前几个坑都是环境问题不是代码逻辑问题。这就再次说明调试大模型调用第一件事永远是确认环境第二件事才是看代码。7. 下一步你的第一个“真·Agent”可以这样做文章写到这里你已经跑通了单次调用理解了消息历史机制也知道工具调用是怎么一回事了。那下一步该做什么我给你的建议是别急着上框架先用裸代码实现一个最简单的“记忆增强型问答机器人”然后给它加一个工具调用。7.1 版本一带对话历史的终端聊天机器人改造方向很简单就是维护一个 messages 列表然后在循环里不断接收用户输入发送请求把回复追加进历史。代码量也不大30 行到 40 行就够。这个版本能帮你把“消息历史管理”这个手感练出来。7.2 版本二接入计算器工具在版本一的基础上给请求体加上tools参数让模型在遇到数学题时返回tool_calls。你的代码判断到finish_reason tool_calls之后就用 Python 的eval或者ast模块执行表达式然后把执行结果作为role: tool的消息追加进对话历史再次请求模型。这一步跑通之后你就体验到了 Agent 的精髓模型不是直接回答数学问题而是“指使”代码去计算结果再把结果包装成回答。这个“指使”与“执行”的分离就是 Agent 与普通聊天机器人的分水岭。7.3 版本三多工具路由当你有了计算器、天气查询、新闻搜索等多个工具之后模型会根据用户的问题自动选择调用哪个工具。这就是“路由”的概念。此时你可以考虑把工具注册成字典键是工具名值是对应的处理函数tools_map { calculate: calculate_func, get_weather: get_weather_func, search_news: search_news_func, }模型返回tool_calls后你只需for call in tool_calls: func_name call[function][name] args json.loads(call[function][arguments]) result tools_map[func_name](**args)这一步你已经具备了自己写一个极简 Agent 框架的雏形了。所谓 Agent 框架无非是把这套循环逻辑、工具注册机制、消息管理机制工程化、插件化而已。回到开头说的那个问题——为什么我坚持手撸因为这套循环你亲手写过一遍之后你再看 LangChain 的文档就像看一个“你早就写过的代码”被别人包装成更优雅的形式。你理解它的设计动机而不是被它的抽象绕晕。这就是基本功的价值。如果你打算把 Agent 这条线学深我的建议是先把裸代码跑熟到能实现版本三再回头去学 LangChain 等框架效果会翻倍。别问为什么去试一次你就懂了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门