AI Agent | 和 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 · 原理三:那些参数到底在干嘛
temperature、top_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 应用几乎必须用流式?三个理由:
-
体验:几十秒的生成如果一次性返回,用户早就跑了
-
可观测:流式让你看到模型"正在干什么",出现异常能早点发现
-
可中断:用户在生成中途可以点"停止"——非流式做不到
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.*;
public class ChatApiDemo {
private static final String URL = "https://api.deepseek.com/chat/completions";
private static final String API_KEY = System.getenv("DEEPSEEK_API_KEY");
public static void main(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 推送,打字机效果的真相
-
工程化:重试、超时、监控、换模型
下一期聊聊【向量库 & Embedding】——这也是我们 AI Agent 学习路线的下一站。想继续的学习的点个【赞】和【推荐】让主编知道!
点个【关注】,私信主编,获取一手的面试八股、编程教程、效率工具、AI Agent等学习资料。、
更多推荐

所有评论(0)