AIGC标识 抓包 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 秒

几个一眼可见的事实:

  1. 我的话在输入里占比不到千分之一。剩下 99.9% 是系统提示词、上下文注入和那 26 个工具的定义——这就是 agent 应用的"固定开销",不管你问什么,每个请求都全额支付。
  2. 处理输入比生成输出贵一个数量级(11.5s vs 0.4s)。直观印象里"模型在思考怎么回答"很慢,实际大头是"把 2000 个 token 的上下文重新读一遍"。这也解释了为什么 agent 多轮任务越聊越慢:历史每轮都在变长。
  3. 输入和输出速度差近 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

posted @ 2026-10-23 23:09  朱瑞福  阅读(3)  评论(0)    收藏  举报