☰
大模型API调用实战:从HTTP原理到Agent开发地基
2026/10/17 4:18:45 网站建设 项目流程

做Agent开发有一条绕不开的水路:把大模型的能力真正接到自己的代码里。这个系列写到第20篇,我特地停下来聊一个最基础、很多人却一直没彻底吃透的话题——LLM的基础使用。具体说,就是那层让所有Agent跑起来的底层接口:API调用。无论是OpenAI的GPT,还是国内几家主流的国产大模型,表面看参数、界面各不一样,底子却都长在同一套思路上。只要把这套逻辑理顺,后面再复杂的Agent编排,对你来说都只是脚手架而已。

这篇内容适合两类人:一类是刚接触大模型开发、还没正经调过API的程序员,另一类是已经在用现成框架、但对接口细节一直模棱两可的人。我不打算只给一段能跑的代码就完事,而是把请求、响应、多轮对话、国产模型切换、流式输出、常见坑位全部过一遍。读完之后,你写代码时会有一种“原来就这”的通透感。

1. 为什么这条Agent技术栈里,API调用才是最该先吃透的一环

先说个有点反常识的判断:很多人在Agent项目里花大量时间研究提示词、研究各种编排框架,最后卡住的反而是一行API调用。原因很简单,整个Agent链条里的工具调用、记忆读写、任务拆解,最终都要变成一次次的模型请求。模型请求这层地基要是心没底,上面搭什么都是歪的。

框架当然能用。LangChain、各类面向Agent的SDK,它们确实把很多重复劳动封装掉了。但封装带来便利的同时,也会遮蔽掉不少关键信息。你遇到一个奇怪的报错,框架帮你把错误包装了一层又一层,排查起来反而更费劲。我自己的习惯是:先用裸HTTP请求把原理跑通,再看框架源码时就一清二楚了。这也是为什么这篇我要从最原始的curl开始讲,而不是直接丢给你一个SDK。

再换一个角度。A股之外的Agent开发圈子里,几乎每过几个月就会冒出一个新模型、一个新接口。OpenAI的接口改过版,不少国产大模型也在快速迭代自己的端点。但它们的核心交互方式没有变:你发一段JSON,它还你一段JSON。把这个底层模式吃透,新模型对你来说只是一次配置更新,而不是一次重新学习。

另外一个常常被忽略的理由是成本。Agent项目跑起来之后,一次任务可能要连续调用几十甚至上百次模型接口。如果你只会在框架里叠Node,不看每一次请求实际传了什么、返回了什么,很容易在不知不觉中把上下文撑爆、把token花光。很多同学问“为什么我的账单这么高”,答案往往就藏在某次请求的messages数组里。这些都在API层才能看清楚。

所以,这一篇的定位不是“API手册搬运”,而是“帮你把底层逻辑焊死在脑子里”。后面讲到的很多细节,所有Agent开发场景都会反复用到。

2. 从一次最原始的HTTP请求看懂LLM接口的本质

我们先把所有封装都剥掉,用最原始的方式调一次大模型接口。这里以OpenAI的chat completions接口为例,它也是目前绝大多数模型接口的事实标准。

2.1 一次curl请求,看清请求结构

curl https://api.openai.com/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer sk-你的密钥" \ -d '{ "model": "gpt-3.5-turbo", "messages": [ {"role": "system", "content": "你是一个逻辑严谨的助手。"}, {"role": "user", "content": "一句话解释什么是KV存储。"} ] }'

这段代码里藏着三个最核心的知识点。

第一个是鉴权方式。API密钥通过Authorization请求头传递,格式固定是Bearer加空格加密钥。密钥是你在平台后台创建的,建议用环境变量管理,别硬编码进代码里,更别不小心提交到仓库。

第二个是请求体结构。真正承载对话内容的是一个叫messages的数组,数组里的每个元素都带一个role字段和content字段。role决定某段文本是谁说的,常见有system、user、assistant三种。system用来设定整个对话的基调或行为规则,user是用户输入,assistant则是模型之前的回复。接口本身是无状态的,你每次都要把完整的messages数组传过去,模型才会“记得”前文。

第三个是模型名称。不同模型的能力、价格、上下文长度都不一样。OpenAI的gpt-3.5-turbo属于性价比较高的通用模型,gpt-4系列更强但更贵。选哪一款取决于你的场景,以后面试或者接手别人项目时,第一件事就是确认对方用的什么模型。

2.2 响应结构:真正有用的是choices

请求发出去后,返回的JSON大概是这样的结构:

{ "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1699999999, "model": "gpt-3.5-turbo", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "KV存储就是用键(Key)找到值(Value)的存储方式..." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 31, "completion_tokens": 42, "total_tokens": 73 } }

真正要拿的回复在choices[0].message.content。为什么是choices数组而不是直接一个字段?因为有些场景下你会请求多个候选回复,或者做函数调用时会返回多个选项。大多数时候我们只取第一个。

finish_reason也值得关注。常见值有stop(正常结束)、length(因为达到token上限被截断)、content_filter(内容被安全策略过滤)、tool_calls(模型要求调用工具)。其中length非常坑,说明模型想说但没说完整,你的输出长度限制把它掐断了,需要调高max_tokens或精简上下文。

usage字段记录了这次调用的token消耗。很多初学者完全忽略它,等到月底看账单才追悔莫及。我在写代码时通常会把这个字段存日志,方便后续统计成本。

2.3 为什么建议先手写一遍requests调用

SDK的本质,就是把上面这些HTTP细节包装得更友好而已。但如果你直接学SDK,很可能连“messages是个数组”“回复在choices[0]”这种基本事实都没概念。一旦遇到SDK版本升级、方法名变更,你会变得很被动。

我在带人入门的时候,都会要求先用requests库手写一遍。代码不长,十几行:

import requests def chat_once(api_key, content): resp = requests.post( "https://api.openai.com/v1/chat/completions", headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", }, json={ "model": "gpt-3.5-turbo", "messages": [ {"role": "system", "content": "你是一个严谨的助手。"}, {"role": "user", "content": content}, ], }, timeout=30, ) resp.raise_for_status() data = resp.json() return data["choices"][0]["message"]["content"] print(chat_once( api_key=os.getenv("OPENAI_API_KEY"), content="帮我列一个Agent项目的目录结构", ))

这里有个小细节:我加了timeout=30。别小看这个参数,模型接口的响应时间并不稳定,有时候几秒,有时候十几秒。不设timeout的话,你的程序可能会挂着等很久。同理,raise_for_status()会在HTTP状态码为4xx或5xx时直接抛异常,避免你拿到错误响应体还在硬解析。

3. 用Python实现对话续接、角色分离与上下文管理

一次性调用只能算热身,Agent场景下更常见的是连续对话。你要让模型记住刚才聊了什么,又不能真的“教会”它记忆,唯一的办法就是每次请求把历史消息原封不动地带过去。

3.1 多轮对话其实是一道加法题

下面这个循环就是一个最简单的带上下文聊天程序:

messages = [ {"role": "system", "content": "你是一个耐心的技术助手,回答尽量简洁。"}, ] while True: user_input = input("你:") if user_input.strip().lower() in ("exit", "quit"): break messages.append({"role": "user", "content": user_input}) resp = requests.post( "https://api.openai.com/v1/chat/completions", headers={"Authorization": f"Bearer {api_key}"}, json={ "model": "gpt-3.5-turbo", "messages": messages, "temperature": 0.7, }, timeout=30, ) data = resp.json() assistant_reply = data["choices"][0]["message"]["content"] messages.append({"role": "assistant", "content": assistant_reply}) print("助手:", assistant_reply)

每次用户输入后,我们把它加入messages,然后整体发给模型;模型回复后,再把回复也加入messages。下一轮用户输入进来,数组里就包含了完整的历史。这个机制非常像“把聊天记录贴在对话框上方”。

3.2 system角色是Agent人格的开关

实际项目里,system prompt的重要性往往远超user input。你可以把system看成一段“永久生效的背景设定”,用户无论问什么,模型都会在这个框架内作答。比如做个客服Agent,你可以在system里规定语气、红线、服务边界;做个代码审查Agent,你可以在system里要求它按规范输出问题清单。

一个很多新手会踩的坑是:想更新system prompt,却直接往messages末尾再塞一条role为system的消息。这不太推荐。有些模型对system位置和数量很敏感,行为可能变得不稳定。更稳妥的做法是改写messages数组里最早的那条system消息,让它始终位于第一位。

3.3 上下文长度的硬边界

这个方案有个致命短板:messages数组会无限增长。大模型对上下文长度有上限,比如某些模型的窗口是129K token,一旦历史消息超过这个数,请求直接报错。更现实的问题是,消息越长,单次请求的延迟和费用越高。

所以,工程上通常有三种处理手段:

  • 截断法:只保留最近N轮对话,前面的历史丢弃。
  • 摘要法:把早期对话让模型总结成摘要,用摘要替换原文。
  • 分层法:核心信息放system,最近对话完整保留,远期内容压缩成摘要。

我自己做实际项目时,最常用的是“摘要法”和“截断法”混合。简单说,维护一个“上下文窗口”,窗口满了就把最老的若干轮丢给模型做一个压缩摘要,再把摘要塞回system区域,继续对话。这个操作在Agent里通常叫“记忆管理”,但底子还是这里说的messages操作。

4. 国产大模型的一行切换:BaseURL与model才是唯二变量

国内做应用开发,十有八九要接国产大模型。好消息是,目前主流的国产大模型服务商普遍提供了与OpenAI协议兼容的接口。这意味着,你之前为OpenAI写的Python调用代码,可以几乎原封不动地切换到国产模型上。

4.1 OpenAI兼容接口是怎么一回事

所谓兼容,就是对方把接口的路径、请求格式、响应格式都对齐到/v1/chat/completions这套协议上。这样一来,社区里的工具链、SDK、开源项目都能无缝切换。具备SDK换厂商,你只需要改两个地方:base_url和api_key,有时还要改model名称。

拿Python的openai库举例。很多人以为openai这个包只能连OpenAI官方,其实它早就可以通过base_url指向任意兼容端点。代码如下:

from openai import OpenAI client = OpenAI( api_key="你的国产模型API Key", base_url="https://api.xxx.com/v1", ) resp = client.chat.completions.create( model="model-name", messages=[ {"role": "system", "content": "你是Agent的核心大脑。"}, {"role": "user", "content": "帮我规划一个调研任务。"}, ], ) print(resp.choices[0].message.content)

这里的api_key是你在国产模型开放平台创建的密钥,model则要根据各家文档填。修改base_url时注意两点:第一,很多厂商要求最后的路径带/v1,不带的话可能404;第二,别在base_url末尾加多余的斜杠,拼接时容易变成双斜杠导致签名或路由异常。

4.2 主流国产模型的接入配置速查

下面这张表整理了几家常见的大模型API接入信息,具体以厂商最新文档为准。模型名和端点会更新,思路不变。

厂商/模型兼容BaseURL常见模型名示例说明
智谱https://open.bigmodel.cn/api/paas/v4glm-4-flash / glm-4-plus对话、工具调用支持完整
DeepSeekhttps://api.deepseek.com/v1deepseek-chat / deepseek-reasoner性价比突出,长上下文很实用
阿里云百炼(通义)https://dashscope.aliyuncs.com/compatible-mode/v1qwen-plus / qwen-max兼容模式下用OpenAI SDK即可
月之暗面(Kimi)https://api.moonshot.cn/v1moonshot-v1-8k / moonshot-v1-128k长文本场景比较合适

接国产模型的时候,我的建议是先看清三个参数:base_url、api_key、model。这三个齐了,代码基本就能跑。不少国产平台本身也提供自己的SDK,但如果你已经写好了一套兼容OpenAI协议的工具链,真的没必要为每个厂商单独引入SDK,统一走兼容协议更省心。

4.3 切换模型时的“隐藏差异”

别以为协议兼容就万事大吉。不同模型对参数的表现差异很明显。最典型的是temperature。同一个temperature值,在A模型上可能表现温和,在B模型上就可能过于发散或者过于保守。换成新模型后,建议先做一轮小规模的输出质量回归测试,别直接上生产。

另外,不同模型的tokenizer也不一样。同一个汉字、同一段代码,在不同模型那里占用的token数量可能有差别。如果你按OpenAI的token估算经验去算国产模型的成本,误差会很大。最靠谱的方式是看响应里的usage字段,以实际返回为准。

还有一个项目里常见的问题:某些国产模型的function calling参数名不完全一致。OpenAI用tools,部分国产兼容接口也用它,但仍有少数平台用functions或者需要额外的参数格式。如果你的Agent依赖工具调用,切换前一定要先看文档,不要想当然地假设“兼容就等于一模一样”。

5. 流式输出与结构化返回:Agent交互里绕不开的两个进阶点

基础打通之后,有两个技能几乎是Agent开发标配:流式输出和结构化返回。前者决定用户体验,后者决定Agent能不能稳定地解析模型输出。

5.1 用流式输出解决“等半天没反应”的问题

模型生成回复通常需要几秒,非流式模式下,调用方要等完整回复生成完毕才能拿到结果。这期间用户只能干瞪眼。流式输出则不同,模型每生成一小段内容,服务器就立即推送过来,前端可以逐字显示,体验会好很多。

OpenAI SDK里的流式调用很简单:

stream = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, stream=True, ) for chunk in stream: delta = chunk.choices[0].delta.content if delta: print(delta, end="", flush=True)

这里要注意的是delta.content。流式响应里,每个chunk携带的是内容片段,不是完整消息。第一次收到的chunk里可能还会带role,后续基本只有content增量。有些同学没做空值判断就取delta.content,很容易遇到None然后报错。

如果是自己手写requests,流式的路子是启用stream参数,然后逐行读取响应体,解析SSE格式的数据。SSE的格式是一行data: {json},遇到data: [DONE]表示结束。SDK把这层也封装好了,但原理要知道,排查问题时能少走弯路。

5.2 让模型输出你想要的JSON

Agent场景下,模型不只是陪人聊天,更多时候是要输出结构化数据给程序解析。比如让模型抽取用户意图、生成任务清单、输出函数参数。过去很多项目用提示词硬逼模型输出JSON,靠正则抽取,效果一言难尽。现在不少大模型接口直接支持response_format或JSON Mode。

resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": "你是一个信息抽取助手,只输出JSON。"}, {"role": "user", "content": "从这段话里提取人名、时间、地点:张伟周三下午在国际会议中心做汇报。"}, ], response_format={"type": "json_object"}, )

开启JSON模式后,模型更倾向于输出合法且规范的JSON,解析时就不再需要一堆“左看右看”的容错补丁。需要说明的是,JSON模式不是万能的,它保证的是格式合法,不保证字段完全符合预期。取值前仍然要做好字段缺失、类型不匹配的兜底处理。

5.3 参数调校:temperature和max_tokens的基本感

temperature是控制随机性的参数,范围一般是0到2。数值越低,输出越确定;数值越高,输出越发散。做抽取、分类、代码生成这类任务,我倾向用较低的temperature(0.1到0.3)。做头脑风暴、文案创作,可以调到0.8甚至更高。Agent开发里最怕的是同一输入时而对时而错,这时候把temperature调低是最快的改善手段之一。

max_tokens则是限制单次回复的最大token数。有个常见误区:以为设置了max_tokens,模型就会尽量输出这么多。其实它只是“上限”,模型如果觉得话说完了,会提前结束。反过来,如果任务复杂、输出很长,max_tokens设小了,回复会被截断,finish_reason会变成length。遇到这种情况,要么调大上限,要么让模型分步输出。

6. 实际调用中高频踩坑:超时、限流、Token配额与成本控制

这一部分,就当是我们边抽烟边聊踩过的坑。API调用本身不难,难的是让它稳定运行在生产环境里。

6.1 状态码背后的真实原因

接口偶尔报错不可怕,怕的是你对着错误提示干瞪眼。整理一份高频状态码对照表,能让排查速度快很多:

状态码含义常见原因及处理
400 Bad Request请求格式错误messages为空、model参数填错、messages内容不合规
401 Unauthorized鉴权失败API Key无效、密钥过期、Authorization头拼写错误
404 Not Found路径或模型不存在base_url路径错误、model名称不在列表里
429 Too Many Requests触发限流并发太高或余额不足,需要退避重试或提额
500/503服务端异常服务商临时故障,稍后重试,但如果持续出现就要反馈

其中429最常见,也最让人头大。不同平台限流策略五花八门,有的按每分钟请求数限,有的按每分钟token数限。处理429的标准动作是“退避重试”:第一次失败等1秒,第二次等2秒,第三次等4秒,按指数增长,最多重试若干次。直接不加延迟地疯狂重试,只会让限流更严重。

6.2 超时设置与重试逻辑

网络请求一定要设超时。我见过不少线上事故,根本不是模型不行,而是某个请求挂着不返回,线程池被打满。建议把connect超时和read超时分开设。connect设短一点,比如10秒;read设长一点,给模型足够的生成时间,比如60秒或更长,流式模式下甚至要单独适配。

重试不能用还在超时的同一批数据盲打。万一第一次请求其实已经成功生成了,只是响应没回来,你再发一次就会造成重复扣费。稳妥的做法是给请求加幂等标识或业务ID,至少要在日志里记录每次请求的消息摘要和usage,方便对账。

6.3 Token成本和上下文长度的“账房思维”

前面提到usage字段,这里展开讲。一次请求的总token数等于prompt_tokens加completion_tokens。平台计费时,输入和输出往往单价不同,输出一般更贵。所以,别把Agent的每一轮思考过程都原封不动塞进上下文里,那些“中间思维”很多是可以丢弃的。

我见过一个跑崩账单的案例:某个Agent任务里,每执行一步就把整个工具的完整文档塞进上下文,上下文越来越长,单次请求成本从几分钱涨到几块钱,跑了几千次任务就把预算吃光了。这个坑的根源不在“调用贵”,而在“上下文管理失守”。控制token数的核心思路只有一个:让进入messages的内容都有必要,已经完成使命的旧内容及时清出去。

6.4 日志里一定要记录的内容

最后啰嗦一句日志。调API的程序,日志里至少要包含:时间戳、模型名、请求的消息条数或token数、响应状态、耗时、usage、finish_reason。有了这些数据,你才能回答三个经典问题:为什么慢?为什么贵?为什么结果不对?很多人排查问题时两眼一抹黑,就是因为当初没记日志。

我自己在写Agent框架时,会封装一层简单的日志中间件。每次模型调用自动打印一行摘要,比如[05-12 10:00:03] model=gpt-3.5-turbo input_tokens=320 output_tokens=150 cost=$0.0012 duration=4.2s finish=stop。这样的日志配合全链路ID,基本能让大多数线上问题在五分钟内定位。

这个系列后面的内容会越来越贴近完整的Agent编排,但无论怎么发展,API调用这层地基始终没变。我自己的体会是:先花半天时间把HTTP层的调用彻底搞懂,比直接去套各种高级框架要划算得多。等你能看着响应JSON就知道发生了什么、看着报错能判断是参数问题还是网络问题时,再上手那些花哨的框架,你会觉得整个世界都变简单了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询