抓包 AI Agent:到底它发了什么给大语言模型
我们看看当 ai agents 说一句“你好”,到底它发送了多少文本给大语言模型。
实验设置
拓扑很简单,一个教科书式的中间人:
AI Agent(本次实验用了DSH 0.2.0-rc.2) ──> 抓包代理(127.0.0.1:11435) ──> ollama(127.0.0.1:11434)
代理是一个一百多行的 Python 脚本,零依赖,功能说明见文末附录:收到请求先落盘,再原样转发给 ollama;响应(包括流式返回)也原样转回去、同时留档。把 DSH 的模型端点从 11434 改到 11435,流量就全部经过它了。
环境:Apple M2 / 16GB / ollama 0.40.1 / qwen2.5:7b(Q4 量化,4.7GB)
请求体解剖
我在 DSH 里顺着之前的谈话,继续发送了一句"你好"。抓下来的请求体:60,916 字节。
它的顶层结构(OpenAI Chat Completions 格式):
{
"model": "qwen2.5:7b",
"stream": true,
"stream_options": {"include_usage": true},
"max_completion_tokens": 8192,
"messages": [ ... 10 条消息 ... ],
"tools": [ ... 26 个工具定义 ... ]
}
messages 数组的组织方式是:第一条固定是 system(系统提示词),中间是对话历史,你最新的一句话追加在末尾。逐条看这次抓到的 10 条消息:
| messages 组成 | 条数 | 规模 | 内容 |
|---|---|---|---|
| 系统提示词(第一条,role=system) | 1 | 5,790 字符 | 定义模型当前扮演的角色、行为规则 |
| 历史里的注入消息(role=user) | 3 | 约 22,500 字符 | 运行时上下文快照(当前时间、工作目录等)和一份可用技能清单——工作台把它们伪装成用户消息插在历史开头。这份注入比整个工具定义数组还大 |
| 真实对话历史(user/assistant 交替) | 6 | 约 120 字符 | 我之前问过的"你好啊"、"用一句话解释什么是 RAG",以及模型的回答,原样都在 |
| 最新请求(最后一条,role=user) | 1 | 2 个字符 | "你好"——本轮唯一的新增内容 |
除了 messages,请求体还有另一个独立的大块——顶层的 tools 数组:26 个工具定义,21,417 字符。每个工具长这样(这是我自编的示意,结构相同):
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市当前的天气情况",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "城市名"}
},
"required": ["city"]
}
}
}
26 个这样的定义——读文件、写文件、执行命令、搜索、派发子任务……每一个都带名称、用途描述和严格的参数 Schema。这部分是 agent 环境的"菜单":模型每轮都要重新读一遍完整菜单,才知道自己能点什么菜。
所以"你好"两个字膨胀成 60KB 的完整账目是:系统提示词 5.8K 字符 + 注入消息 22.5K 字符 + 对话历史 + 工具菜单 21.4K 字符。你的一轮输入里,属于"你"的部分是 2 个字符。
响应体解剖
响应是 SSE 流(text/event-stream)——就是你在所有 AI 产品里看到的"打字机效果"的来源:服务端每生成几个 token 就推一个 chunk,客户端边收边渲染。抓下来的响应是一串 JSON 块,每个块的 delta.content 里带一小段文字:
"您好" → "!" → "有什么" → "可以帮助" → "您的吗" → "?"
拼接起来就是完整回复:"您好!有什么可以帮助您的吗?"
而最后一个 chunk 里藏着一份对调优极其有用的账单——usage 和 timings。
"你好"的完整账单
| 指标 | 数值 |
|---|---|
| 我输入 | 2 个字符 |
| 模型输入(prompt_tokens) | 2,050 token |
| 模型输出(completion_tokens) | 8 token |
| 输入处理耗时 | 11.5 秒(178 token/秒) |
| 输出生成耗时 | 0.4 秒(20 token/秒) |
| 全程 | 约 12 秒 |
几个一眼可见的事实:
- 我的话在输入里占比不到千分之一。剩下 99.9% 是系统提示词、上下文注入和那 26 个工具的定义——这就是 agent 应用的"固定开销",不管你问什么,每个请求都全额支付。
- 处理输入比生成输出贵一个数量级(11.5s vs 0.4s)。直观印象里"模型在思考怎么回答"很慢,实际大头是"把 2000 个 token 的上下文重新读一遍"。这也解释了为什么 agent 多轮任务越聊越慢:历史每轮都在变长。
- 输入和输出速度差近 9 倍(178 vs 20 token/秒)是本地推理的常态——生成是串行的逐 token 计算,处理输入可以大规模并行。这个不对称性是所有 LLM 应用的性能账本基础。
一个留下的疑点
细心的读者可能已经发现了:60KB 的请求体,粗算应该有一万多 token(工具定义和系统提示词是英文 JSON 为主),但 ollama 报告的 prompt_tokens 只有 2,050——对不上。
原因:上下文窗口(context window)。模型一次能"看见"的 token 数是有上限的,超出部分的常规处理就是截断——而 ollama 默认的窗口很小(我这版是 4096)。也就是说,这次调用里模型并没有看到完整的请求:相当一部分系统提示、工具定义和历史,在进模型视野之前就被裁掉了。它是带着一小截上下文在回答你的。
这对本地部署是个实打实的坑:agent 环境的请求动辄上万 token,不显式调大上下文窗口(ollama 可通过 OLLAMA_CONTEXT_LENGTH 环境变量或模型参数 num_ctx 设置),模型就一直处于"半瞎"状态,而且没有任何报错提示——只会表现出莫名其妙的答非所问和工具调用失败。
附录:抓包代理的功能说明
实验用的代理我就不贴源码了——总共一百多行 Python,把这页功能描述丢给任何一个主流大模型(我这次用的是 GLM-5.3),几秒钟就能生成一份可直接运行的实现,你顺便还能体验一次"用 AI 造工具来研究 AI":
- 拓扑:监听本机 11435 端口,把收到的所有请求原样转发给本机 11434(ollama)
- 请求留档:每收到一个请求,把 method、path、body 落盘成美化后的 JSON 文件(
req_0001.json、req_0002.json……按序编号) - 响应留档:把模型的响应原样转回客户端的同时也留档;流式响应(SSE)会解析成逐 chunk 的列表,方便直接阅读
- 流水日志:另写一份
capture.jsonl,每行一个 JSON 事件记录每次请求/响应的元数据(时间、大小、耗时),方便用jq统计分析 - 实现要点:只依赖 Python 标准库(
http.server+http.client);用多线程处理并发;响应要边收边转发(流式透传),不能攒完再发——否则打字机效果会变成"卡半天蹦一整段"
把工作台的模型端点从 11434 改到 11435,流量就全部经过它;实验完改回来即可。
2026-10

浙公网安备 33010602011771号