一次完整的 AI 对话发生了什么
拆开 HTTP 请求看到的真相
你在 ChatGPT 网页里敲下"帮我写个删除 .DS_Store 的 Python 脚本"按回车。0.5 秒后字符开始一个个蹦出来。3 秒后回复完成。你复制代码、贴到终端、跑通了。
这是 AI 产品最熟悉的一幕——每天数亿次。但这 3 秒之内发生了什么?绝大多数人——包括很多做 AI 产品的工程师——没真正拆开看过。
不拆开看的代价是——你永远不知道为什么 AI 行为奇怪。AI 给错答案了?你只能猜。AI 突然慢了?你只能等。AI 突然行为变了?你只能怀疑模型升级。
把这 3 秒拆开——你会看到 AI 产品的"物理底层"。所有上层的复杂性——RAG、Agent、Multi-modal——都建立在这个底层之上。理解这一层,你才能看透任何 AI 产品。
一、你按下回车——你的产品做的第一件事
ChatGPT 网页前端检测到你按了回车。它做的第一件事是——把你刚才打字的内容打包成一个 HTTP 请求——发给 OpenAI 的后端。
这个请求大致长这样:
POST https://api.openai.com/v1/chat/completions
Authorization: Bearer sk-xxxxx
Content-Type: application/json
{
"model": "gpt-4o",
"messages": [...]
}
注意几件事——
HTTP 请求——是的,AI 产品的底层是普通的 HTTP。没有什么"AI 通道"——就是 RESTful API。
Authorization header——OpenAI 用 API key 鉴权。你的请求带着一个 sk-xxx 开头的 key——告诉 OpenAI"这是哪个账户在用"——用来记费用。
messages 字段——这里是关键。它不是只包含你刚才打字的内容——它包含这次对话的完整历史。
让我们具体看 messages 包含什么。
二、messages 里到底有什么
如果你这是新会话——第一条消息——messages 数组可能是这样:
"messages": [
{"role": "system", "content": "You are a helpful assistant..."},
{"role": "user", "content": "帮我写个删除 .DS_Store 的 Python 脚本"}
]
两条消息。
第一条 role: "system"——这是系统提示词(system prompt)——告诉 LLM "你是谁、要做什么"。你没设置它——但 ChatGPT 内部塞了一个默认的(具体内容 OpenAI 保密)。
第二条 role: "user"——你刚才打字的内容。
如果你已经聊了一阵子——比如 5 个来回——那么 messages 数组里有 11 条消息:
"messages": [
{"role": "system", "content": "..."},
{"role": "user", "content": "你叫什么"},
{"role": "assistant", "content": "我叫 ChatGPT..."},
{"role": "user", "content": "我喜欢吃苹果"},
{"role": "assistant", "content": "苹果很健康!"},
{"role": "user", "content": "我还喜欢吃香蕉"},
{"role": "assistant", "content": "香蕉也是不错的选择..."},
{"role": "user", "content": "我喜欢什么水果"},
{"role": "assistant", "content": "你提到喜欢苹果和香蕉"},
{"role": "user", "content": "帮我列个清单"},
{"role": "assistant", "content": "..."},
{"role": "user", "content": "帮我写个删除 .DS_Store 的 Python 脚本"} // 你刚说的
]
12 条消息——5 轮你说的 + 5 轮 AI 说的 + 1 条 system + 1 条你刚说的。每条都被打包进这次请求。
这就是 0.1 章讲的"Chatbot 维护对话历史"的具体含义——前端或ChatGPT 服务器保存了所有历史 message——每次新请求都塞回去——让 LLM 看到完整 context。
LLM 自己不记得任何东西。它每次请求都从零开始读这 12 条消息——综合理解后续写"第 13 条"(assistant 的回复)。
如果你的对话已经聊了 100 轮——这个 messages 数组有 200 条——每次发请求都把这 200 条全发过去。这是为什么长对话输入 token 累积巨大——它们每个 turn 都重复传送。
三、请求到达 OpenAI 服务端——发生了什么
OpenAI 接收到你的 HTTP 请求——服务端做几件事。
第一件:鉴权。验证你的 API key 有效、账户有余额、没超 rate limit。
第二件:路由。根据 model 字段("gpt-4o")找到对应的模型。GPT-4o 跑在某个 GPU cluster 上——OpenAI 内部有调度系统把你的请求分配到一个有空的 GPU 实例。
第三件:tokenization。把你的 messages 数组转换成 token 序列。GPT 的 tokenizer 看着字符串——按它的词汇表切成 token ID。
"帮我写个删除 .DS_Store 的 Python 脚本" 大概被切成 15-20 个 token。整个 messages 数组(12 条)大概是几百到几千 token。
LLM 内部只认 token——不认字符串。
第四件:embedding。每个 token ID 被映射成一个多维向量——这是 LLM 内部的"语义表示"。"猫"和"狗"的向量在某些维度上相近(都是动物)——跟"苹果"的向量更远。
embedding 是模型权重的一部分——是模型训练时学到的。
第五件:推理。模型经过几十层 transformer 计算——综合理解整个 context——开始生成第一个新 token。
这是 0.5 秒首 token 延迟的来源——前 4 步加上推理的初始化大概要这么久。
第六件:流式输出。模型不是一次性输出完整回复——它是一个 token 一个 token 地生成——边生成边推流回前端。这就是你看到字符一个个蹦出来的原因。
四、流式响应是怎么传回来的
OpenAI 的响应不是一次性的 JSON——它是 SSE 流(Server-Sent Events)。
具体说,OpenAI 服务端开始往你建立的 HTTP 连接里持续推数据——每个 token 一个 chunk。chunk 长这样:
data: {"choices":[{"delta":{"content":"#"}}]}
data: {"choices":[{"delta":{"content":"!/usr"}}]}
data: {"choices":[{"delta":{"content":"/bin"}}]}
data: {"choices":[{"delta":{"content":"/env"}}]}
data: {"choices":[{"delta":{"content":" python"}}]}
...
每个 data 行是一个 token——前端接收到、立刻渲染到屏幕。
为什么用流式?两个原因——
原因 1:用户体验。一次性 3 秒后蹦出整段——用户觉得 AI 卡死了。流式让用户立刻看到反馈——感觉 AI 在思考、在工作。
原因 2:早停可能。如果用户中途按 Ctrl+C 或关掉浏览器——服务端能立刻断连——节省没必要的推理。
流式的代价——前端要写状态机解析每个 chunk、累积成完整文本、处理网络抖动。比一次性 JSON 复杂。
五、tool calling 把这个循环复杂化了
到此为止是Chatbot 形态——文字进文字出。如果是 Agent 形态——还有一层复杂——tool calling。
Agent 在发请求时多塞一个 tools 字段:
"tools": [
{"name": "read_file", "description": "...", "parameters": {...}},
{"name": "bash", "description": "...", "parameters": {...}}
]
LLM 看到 tools 清单——它在生成响应时有两种选择:
选择 A:输出纯文本(像 chatbot)
{"choices":[{"delta":{"content":"This is the answer..."}}]}
选择 B:输出工具调用请求
{"choices":[{"delta":{"tool_calls":[{"name":"read_file","arguments":"{\"path\":\"main.py\"}"}]}}]}
注意 tool_calls 字段——这是 LLM 在说"我想调用 read_file 工具"。
Agent 后端收到这种响应——真的执行那个工具(fs.readFile("main.py"))——拿到结果——再发一次请求给 LLM。这次的 messages 里多了一条"tool result"消息:
{
"role": "tool",
"tool_call_id": "call_abc",
"content": "[文件内容...]"
}
LLM 看到文件内容——继续思考。可能再调工具、可能给最终回复。
这个循环可以跑 10 轮、50 轮、100 轮——直到 LLM 不再请求工具——只输出最终文字回复。
每一轮都是一次完整的 HTTP 请求 + 一次流式响应。10 轮意味着 10 次 HTTP 调用——每次都传完整 messages 历史——成本累积巨大。
这就是为什么 Agent 比 Chatbot 贵——它跑很多轮,每轮 token 都要付钱。一次 Agent 对话可能消耗 50K、100K token——一次 Chatbot 对话可能只 1-2K。
六、opencode 跑这个流程时的具体形态
我们看 opencode 一次完整的"修 bug"任务在 HTTP 层是什么样。
用户输入"修一下 src/auth/login.ts 的密码验证 bug"。opencode 后端组装请求——发给 Anthropic(假设用 Claude):
Turn 1(首次请求):
Request: messages=[system, user]
tools=[read, edit, bash, ...]
Response (stream): "Let me look at the file." + tool_call(read, "src/auth/login.ts")
opencode 后端检测到 tool_call——读文件——拿到 200 行代码。
Turn 2:
Request: messages=[system, user, assistant(text+tool_call), tool_result(文件内容)]
tools=[...]
Response (stream): "I see the issue at line 45." + tool_call(edit, ...)
opencode 后端执行 edit——文件被修改。
Turn 3:
Request: messages=[system, user, ..., tool_result(edit 成功)]
tools=[...]
Response (stream): "Now let me run tests." + tool_call(bash, "npm test")
opencode 跑测试——拿到 stdout。
Turn 4:
Request: messages=[..., tool_result(测试结果)]
tools=[...]
Response (stream): "Tests pass. The bug is fixed." // 纯文本,没 tool_call
LLM 不再请求工具——主循环退出——用户看到最终回复。
整个任务 4 个 HTTP 请求 + 3 次工具执行 + 1 次最终回复。
每个 HTTP 请求都是完整的 messages 历史——所以最后那次请求的 messages 数组里有:
- system prompt(约 5K token,每次重复)
- user message("修 bug")
- assistant turn 1 + tool_call
- tool_result(200 行文件内容,约 2K token)
- assistant turn 2 + tool_call
- tool_result(edit 成功,几十 token)
- assistant turn 3 + tool_call
- tool_result(npm test 输出,可能很大)
加起来 10-20K input token。Claude 3.5 Sonnet 价格 $3/M input token——这一次请求大概 $0.03-0.06。整个任务 4 次请求——大概 $0.10-0.20。
这是 Agent 的"经济学底层"——每次 turn 都要把累积的 messages 全发一遍——token 消耗随 turn 数累积。
七、HTTP 层透露的设计选择
把这一层拆开看——你能立刻识别 AI 产品的设计选择。
ChatGPT 跟你聊天时——它发了什么 system prompt 给 LLM?OpenAI 不公开——但你能猜测大概在做"defining the assistant's behavior"。
Cursor 改你的代码时——它给 LLM 看了哪些工具?你能从 Cursor 的能力反推——它至少有 read_file、edit_file、grep、终端 tool。
Claude.ai 的 artifacts 功能怎么实现的?它定义了一个特殊"artifact"格式——LLM 输出符合这个格式就显示成 canvas。
每个 AI 产品的核心设计——都体现在它发给 LLM 的请求里。HTTP request body 是 AI 产品最诚实的镜子。
opencode 是开源的——你能直接看到它的请求长什么样。每个 prompt 文件(anthropic.txt、gpt.txt 等)就是 system 字段的内容。每个 tool 描述(read.txt、edit.txt 等)就是 tools 字段的内容。后面 10 篇我们会反复看这些文件——它们是 opencode "怎么跟 LLM 沟通"的具体记录。
八、为什么 debugging AI 产品要看 HTTP 层
最后说一件实战的事——你做 AI 产品时最有用的 debugging 技能是看 HTTP 请求。
你的 AI 产品出问题了——AI 不按预期工作。可能的原因:
- system prompt 写得不对
- 历史 message 里有奇怪内容
- tool 描述模糊让 LLM 选错
- tool result 格式不对让 LLM 误解
这 4 种问题——你只能通过看 HTTP 请求 body 才能识别。
如果你只在 UI 层看——你只看到"AI 给错答案了"——不知道为什么。
如果你看 HTTP 请求——你能看到 LLM 实际收到了什么——立刻发现"啊原来 system prompt 里多了一句奇怪的话"或者"啊 tool description 太模糊"。
这是 AI 工程师最基本的技能——学会拦截、log、分析 LLM API 请求。
opencode 在这件事上做得不错——它把每个 LLM 调用 log 出来——你能 replay、analyze、调试。如果你做 AI 产品——给自己留这个能力。不要让 LLM 调用是黑箱——会让你在 debug 时无能为力。
下一章 0.3 我们看 LLM API 里最重要的资源——token。理解 token 才能理解 AI 产品的"经济学"。