乐于分享
好东西不私藏

AI Agent学习之路 | 第一阶段03 | 和 AI 聊天,其实是一次 HTTP 请求(API调用实现)

AI Agent学习之路 | 第一阶段03 | 和 AI 聊天,其实是一次 HTTP 请求(API调用实现)

和 AI 聊天,其实是一次 HTTP 请求

导读:你发出去的每句话,最终都变成一个 JSON 包,POST 到某个服务器的接口上;AI 的回答,也不过是服务器返回的另一个 JSON。这篇文章把大模型 API 调用从头拆到尾——请求长什么样、响应怎么解析、流式怎么实现、Java 代码怎么写,读完你就不会再对着 API 文档发怵了。


01 · 先看一段"原始"的 AI 对话

平时我们用 ChatGPT、用 DeepSeek,看到的是一个漂亮的聊天界面。但把界面扒开,底下发生的事非常朴素:

你发一句话 → 程序把它包装成一个 HTTP 请求 → POST 到模型服务器 → 服务器生成回答 → 以 JSON 返回 → 程序解析展示。

就这么简单。真要说有什么特别的,也就是"你发的每句话都变成了一个 JSON"。下面这段 curl 就是一个完整的大模型调用,复制到终端里换上你的 Key 就能跑:

curl https://api.deepseek.com/chat/completions \  -H "Content-Type: application/json" \  -H "Authorization: Bearer sk-你的Key" \  -d '{    "model": "deepseek-v4-flash",    "messages": [      {"role": "system", "content": "你是一个简洁的助手"},      {"role": "user", "content": "你好,用一句话介绍自己"}    ],    "temperature": 0.7,    "stream": false  }'

看到没?没有魔法。就是 curl + 一个 POST + 一段 JSON。我们把这东西拆开,看看每一块都是干嘛的。


02 · 拆解请求:你发的那句话变成了什么

请求体是一个 JSON,核心字段就这几个:

{"model""deepseek-v4-flash","messages": [    {"role""system",    "content""你是一个简洁的助手"},    {"role""user",      "content""你好"},    {"role""assistant""content""你好!有什么可以帮你?"},    {"role""user",      "content""介绍一下你自己"}  ],"temperature"0.7,"max_tokens"2048,"stream"false}
字段
作用
通俗理解
model
用哪个模型
点菜时选"哪个厨师"
messages完整对话历史
你递给 AI 的一沓纸条
role
每条消息是谁说的
system=规则 / user=用户 / assistant=AI
temperature
回答的随机程度
0=老实人,1=放飞自我
max_tokens
最多生成多少 token
给回答设个字数上限
stream
是否流式返回
一次性给 vs 边写边给

注意 messages 前面那个"完整"二字。这是整个 API 设计里最反直觉、也最重要的一点,下一节专门讲。


03 · 拆解响应:AI 的回答长什么样

请求发出去,服务器返回一个 JSON,长这样:

{"choices": [    {"message": {"role""assistant","content""你好!我是 DeepSeek,很高兴认识你。"      },"finish_reason""stop"    }  ],"usage": {"prompt_tokens"12,"completion_tokens"35,"total_tokens"47  }}

要拿的东西只有两处:

  • 回答choices[0].message.content——AI 说的话
  • 花费usage.total_tokens——这次调用烧了多少 token(就是花了多少钱)

choices 是个数组而不是单个对象,是因为有的接口支持一次返回多个候选(n 参数)。日常使用直接取第 0 个就行。


04 · 原理一:为什么"多轮对话"要自己拼历史

这是新手最容易踩的坑,值得单独说。

大模型 API 是无状态的。 每一次调用,模型都像第一次见你——它不记得上一轮你们聊了什么。你看到的"它记得",全靠客户端(你的代码)把历史一字不差地塞进 messages 数组再发过去。

类比:大模型像个"健忘的作家",你每次给它递一沓纸条,它只看这沓纸条。想让"对话"连续,你就得每次把之前所有纸条重新递一遍。

所以多轮对话的代码长这样:

// messages 就是"对话历史",由客户端自己维护List<Map<String, String>> history = new ArrayList<>();history.add(Map.of("role""system""content""你是一个简洁的助手"));// 第 1 轮:用户提问history.add(Map.of("role""user""content""你好,我叫小明,请记住我"));String r1 = callApi(history);history.add(Map.of("role""assistant""content", r1));   // 回答也追加进去// 第 2 轮:模型"记得"小明,因为历史里全都有history.add(Map.of("role""user""content""我叫什么名字?"));String r2 = callApi(history);   // 能答对:小明

这个设计也解释了两个生产环境常见问题:

  • 上下文超限:历史越长,请求越大。模型有上下文窗口上限(比如 128K token),超出就会报错。所以 Agent 框架都要做"历史压缩/裁剪"。
  • 为什么无状态反而是好事:服务器不用维护海量会话,水平扩展毫无压力。把"记忆"外包给客户端,是工程上的聪明取舍。

05 · 原理二:Token——AI 世界的货币

usage 里的 token 到底是什么?

之前讲 LLM 原理时说过:模型不认文字,只认"词元"(token)。一句话会被分词器切成一串 token,模型按 token 逐个生成。粗略换算:

  • 1 个英文字单词 ≈ 1 个 token
  • 1 个汉字 ≈ 1~2 个 token
  • 100 万 token ≈ 大约 75 万英文单词,或 60 万汉字

token 决定了三件事:

① 计费。 大模型按 token 收费:输入(prompt)和输出(completion)单价不同,一般是输入便宜、输出贵。你在响应里看到的 usage,就是这次调用的"账单"。

② 上下文窗口。 模型的"记忆力"上限用 token 数表示。messages 里的所有历史 + 生成的回答,都算在窗口里。128K 窗口听着大,塞上几十轮对话加几份文档,很快就见底。

③ 输出上限。max_tokens 限制模型最多生成多少 token——相当于给回答设了"页数上限"。不设的话,有的模型能一直写下去。

顺手提一句:max_tokens 管的是"生成多少",不是"总共多少"。请求太长超限会直接报错,这是 RAG 应用里最常见的报错之一,到时候别慌,裁剪历史就行。


06 · 原理三:那些参数到底在干嘛

temperaturetop_p 这些参数,上一期讲 LLM 原理时都埋过伏笔——它们控制的其实是"采样"这一步:模型算出每个 token 的概率分布后,怎么从这个分布里挑一个。

参数
控制什么
怎么调
temperature
概率分布被"压平"还是"变尖"
低(0~0.3)输出稳定,适合事实问答、代码(科学);高(0.7~1)更发散(创意)
top_p
只在概率最高的前 p 部分里采样
和 temperature 二选一调,别同时较劲
max_tokens
生成长度上限
顺手设上,防话痨
stop
遇到这些词就停止生成
可以传数组,控制输出边界

一句话记忆:temperature 越低,模型越"怂",越只敢选最有把握的词;越高越"浪",敢选冷门的词。 想要稳定可靠,就压低;想要惊喜,就拉高。


07 · 原理四:流式输出(SSE)——打字机效果是怎么来的

现在把请求里的 "stream": false 改成 true,神奇的事情发生了。

非流式:模型憋 5 秒、10 秒,一次性把整段回答返回来。你的界面干等,转圈圈。

流式:模型每生成一个 token,服务器立刻推给你一个。你的界面一边收一边显示,效果就是 ChatGPT 那个"打字机"。

流式用的协议叫 SSE(Server-Sent Events),格式很朴素——一行一行 data: 开头的数据,直到 data: [DONE] 结束:

data: {"choices":[{"delta":{"role":"assistant"}}]}data: {"choices":[{"delta":{"content":"你好"}}]}data: {"choices":[{"delta":{"content":"!"}}]}data: {"choices":[{"delta":{"content":"很高兴"}}]}...data: [DONE]

每个 data: 后面是一个 JSON,里面 choices[0].delta.content 就是这一段新生成的增量。把增量拼起来,就是完整回答。

为什么 Agent 应用几乎必须用流式?三个理由:

  1. 体验:几十秒的生成如果一次性返回,用户早就跑了
  2. 可观测:流式让你看到模型"正在干什么",出现异常能早点发现
  3. 可中断:用户在生成中途可以点"停止"——非流式做不到

08 · Java 实战:三种写法,从"裸调"到"一行"

方式一:原生 HttpClient,把原理写明白(理解为主)

不依赖任何 SDK,JDK 自带的 HttpClient 就够了。这也是理解 API 最好的方式:

import java.net.URI;import java.net.http.*;import java.net.http.HttpResponse.BodyHandlers;import com.fasterxml.jackson.databind.*;publicclassChatApiDemo{privatestaticfinal String URL = "https://api.deepseek.com/chat/completions";privatestaticfinal String API_KEY = System.getenv("DEEPSEEK_API_KEY");publicstaticvoidmain(String[] args)throws Exception {        String body = """            {              "model": "deepseek-v4-flash",              "messages": [                {"role": "system", "content": "你是一个简洁的助手"},                {"role": "user", "content": "你好,用一句话介绍自己"}              ],              "temperature": 0.7            }            """;        HttpClient client = HttpClient.newHttpClient();        HttpRequest request = HttpRequest.newBuilder()                .uri(URI.create(URL))                .header("Content-Type""application/json")                .header("Authorization""Bearer " + API_KEY)                .POST(HttpRequest.BodyPublishers.ofString(body))                .build();        HttpResponse<String> response = client.send(request, BodyHandlers.ofString());// 用 Jackson 解析:取回答 + 看花费        ObjectMapper mapper = new ObjectMapper();        JsonNode root = mapper.readTree(response.body());        String reply = root.path("choices").get(0)                .path("message").path("content").asText();int totalTokens = root.path("usage").path("total_tokens").asInt();        System.out.println("AI:" + reply);        System.out.println("本次消耗 token:" + totalTokens);    }}

30 行左右,一个能用的"AI 调用"就完成了。鉴权就是一个 Header(Authorization: Bearer),请求是一个 JSON,响应是一个 JSON——全文的核心就这三句话。

方式二:流式调用,做出打字机效果

把 "stream": true 加上,用 BodyHandlers.ofLines() 按行读 SSE,每来一行就打印增量:

HttpRequest request = HttpRequest.newBuilder()        .uri(URI.create(URL))        .header("Content-Type""application/json")        .header("Authorization""Bearer " + API_KEY)        .header("Accept""text/event-stream")        .POST(HttpRequest.BodyPublishers.ofString(body))   // body 里已带 "stream": true        .build();client.send(request, BodyHandlers.ofLines())     // 按行读取    .body()    .filter(line -> line.startsWith("data:"))    // 只处理数据行    .forEach(line -> {        String data = line.substring(5).trim();if (data.equals("[DONE]")) {            System.out.println();                // 流结束return;        }try {            JsonNode node = new ObjectMapper().readTree(data);            String delta = node.path("choices").get(0)                    .path("delta").path("content").asText();            System.out.print(delta);             // 边收边打印 → 打字机        } catch (Exception ignored) { }    });

方式三:Spring AI,一行搞定

原理懂了之后,你会觉得框架真香。同样的功能,Spring AI 里就三行:

String reply = chatClient.prompt()    .system("你是一个简洁的助手")    .user("你好,用一句话介绍自己")    .call()    .content();

流式也只要把 .call() 换成 .stream()messages 的拼装、鉴权、JSON 解析、SSE 处理,框架全包了。

三种方式怎么选

方式
代码量
灵活性
适用场景
原生 HttpClient
30~50 行
最高
学原理、极简依赖、特殊定制
流式原生
40~60 行
想完全掌控 SSE 处理
Spring AI
3~5 行
中(Advisor 可扩展)
业务项目首选

我的建议:先亲手写一遍方式一,让"HTTP + JSON"的直觉长进肌肉里;正式项目用方式三。别一上来就只写框架——那样你永远不知道 messages 为什么要自己拼。


09 · 工程化:上线前要处理的四件事

API 调通只是开始。真到生产环境,这四个问题你迟早要面对:

① 重试与退避。 429(限流)、5xx(服务端故障)都是家常便饭。标准做法是指数退避:第一次失败等 1 秒,第二次 2 秒,第三次 4 秒……加一点随机抖动,防止一堆请求同时重试把服务打崩。前提是请求要"幂等"——重发同样的请求不产生副作用(大模型调用天然满足)。

② 超时与取消。 必须设置 connectTimeout 和 readTimeout,不然一次网络抖动就能挂住你的线程。用户关掉页面、点了停止,要能真正取消请求——流式场景尤其要处理好连接中断。

③ Token 成本监控。 每个响应里的 usage 字段是免费的"计量表"。上线后记日志、埋点,按月统计成本趋势。很多项目第一个月账单出来才发现"怎么烧了这么多钱"——token 用量和对话轮数直接挂钩,心里要有数。

④ 多模型切换。 OpenAI 兼容协议最大的价值就在这里:base-url 一换,代码不动。今天用 DeepSeek,明天想换通义、智谱,改个地址就行。Spring AI 里更绝,连 starter 依赖一起换掉,业务代码一行不改。


10 · 写在最后

把这篇看完,你其实已经掌握了 AI 应用开发的地基:

  • 请求:一个 HTTP POST + JSON(model / messages / 参数)
  • 响应:choices 里取内容,usage 里看成本
  • 无状态:历史自己拼,这就是对话的本质
  • 流式:SSE 逐 token 推送,打字机效果的真相
  • 工程化:重试、超时、监控、换模型

说明:本文由作者撰写,仅供学习交流,代码基于 Spring AI 2.0 / Spring Boot 4 编写。


下一期聊聊【向量库 & Embedding】——这也是我们 AI Agent 学习路线的下一站。想继续的学习的点个【赞】和【推荐】让主编知道!顺手点个【关注】,感谢各位学习路上的朋友。