创见博客
一次 Agent 任务抓包:LLM 每轮到底收到了什么
七崽爱吃小饼干2026/07/13阅读 3专栏 AI开发

一次 Agent 任务抓包:LLM 每轮到底收到了什么

最近我抓了一次 OpenCode Agent 执行任务的完整网络请求。用户只问了一句「介绍一下这个项目」,但从抓包看,Agent 并不是把这句话直接丢给模型等答案,而是经过了标题生成、主任务规划、本地工具调用、工具结果回灌、最终总结几轮交互。

这篇文章围绕这次抓包展开,重点看三件事:每轮 Agent 会组织哪些内容发给 LLM,LLM 如何回复,以及它如何通过本地工具逐步补齐上下文。

抓包背景

这次抓包文件里,和这次任务相关的请求主要有四条:

  • 85cce2fa633068fd0edf:会话标题生成请求。
  • 662667d10108248b7647:主 Agent 第一轮请求。
  • 1163a2ca3cac64a628d6:主 Agent 第二轮请求,带上第一轮工具结果。
  • 5ea0fffbb7377d2325f2:主 Agent 第三轮请求,生成最终项目介绍。

所有请求都发往同一个接口:

http
POST https://ink.bytedance.net/api/llm/responses

但它们的语义完全不同。标题生成请求很轻,主 Agent 请求则会携带完整 developer prompt、工具 schema、历史消息、工具调用记录和工具返回内容。

第一类请求:给会话起标题

先看 85cce2fa633068fd0edf。这条请求使用的是 gpt-5.4-mini-2026-03-17,请求体很小,核心 input 只有三段:

json
{
  "model": "gpt-5.4-mini-2026-03-17",
  "input": [
    {
      "role": "developer",
      "content": "You are a title generator. You output ONLY a thread title..."
    },
    {
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "Generate a title for this conversation:\n"
        }
      ]
    },
    {
      "role": "user",
      "content": [
        {
          "type": "input_text",
          "text": "介绍一下这个项目"
        }
      ]
    }
  ],
  "stream": true,
  "store": false
}

这条请求没有带主 Agent 的完整 system/developer prompt,也没有工具定义。它只是一个单独的标题生成任务。响应是 SSE 流,模型分两段吐出标题:

text
项目
介绍

最终 response.output_text.done 里的文本是:

text
项目介绍

所以,标题生成和真正执行任务是分开的。它不是创建 session 的接口,也不是主任务推理,只是一次独立的轻量 LLM 调用。

主 Agent 第一轮:把任务、系统规则和工具能力交给模型

真正执行「介绍一下这个项目」的是 662667d10108248b7647。

这条请求使用模型:

json
{
  "model": "gpt-5.5-2026-04-24",
  "reasoning": {
    "effort": "medium",
    "summary": "auto"
  },
  "tool_choice": "auto",
  "stream": true
}

它的 Content-Length 约 136KB。用户明明只输入了 8 个字,但请求体很大,因为主 Agent 第一轮会带上大量运行上下文。

这一轮 input 只有两条:

  • developer:完整 OpenCode 行为规范,约 40238 字符。
  • user:用户原始问题「介绍一下这个项目」。

除了 input,请求还带了 68 个工具定义,包括:

  • read
  • glob
  • grep
  • bash
  • apply_patch
  • task
  • todowrite
  • browser_*
  • webfetch
  • websearch
  • skill

这意味着第一轮模型拿到的不只是用户问题,而是完整的 Agent 操作环境:它知道自己是谁、该如何协作、有哪些工具、工具参数结构是什么、当前工作目录是什么、哪些行为被允许或禁止。

第一轮 developer prompt 的组成

第一轮里最值得拆的是 developer prompt。它总长度约 40238 个字符,并不是单一的 system prompt,而是由多类信息拼接而成。

第一部分是 OpenCode 的通用身份和行为规则,开头大约 948 个字符。它告诉模型:你是 OpenCode,你和用户共享同一个 workspace,你要像务实的软件工程师一样工作,回答要直接、事实化,做事前要先看代码库,不要凭空假设。这里还规定了搜索时优先用 Glob / Grep,能并行就并行,尤其是文件读取。

第二部分是编辑和执行策略,包括这些章节:

text
## Editing Approach
## Autonomy and persistence
## Editing constraints
## Special user requests
## Frontend tasks

这部分约 5142 个字符。它规定了最小正确改动、不要过度抽象、用户没明确只要方案时默认实际执行、不能随便 revert 用户改动、手工编辑必须用 apply_patch、不能用破坏性 git 命令、review 请求要按代码审查方式处理,以及前端任务要避免模板化设计。

第三部分是和用户交互、输出格式、响应通道规则,从 # Working with the user 开始,约 3289 个字符。它规定了不要用 “Got it / Done / Great question” 这类开场,Markdown 不要嵌套 bullets,命令和路径用 inline code,中间进展走 commentary,最终结果走 final,开始重要工作前要简短说明第一步,编辑前要说明即将编辑什么。

第四部分是当前运行环境和项目级指令。环境块里明确给出了:

text
Working directory: /Users/bytedance/code/marketing_fe
Workspace root folder: /Users/bytedance/code/marketing_fe
Is directory a git repo: yes
Platform: darwin
Today's date: Mon Jul 13 2026

紧接着是:

text
Instructions from: /Users/bytedance/code/marketing_fe/AGENTS.md

这说明第一轮 developer prompt 还拼进了目标仓库的 AGENTS.md。这部分从 # 项目介绍 开始,约 3495 个字符,包含项目介绍、名词解释、Hera 相关命令、目录介绍和开发规范。也就是说,在模型真正读取 README.md 之前,它已经从项目级指令里知道了:这是 marketing 前端 monorepo,使用公司内部 emo 管理,技术栈包括 React、TypeScript、Less,并且必须使用中文交流。

第五部分是 available skills 列表,也是最大的一块,约 26869 个字符。它以这样的 XML 风格结构出现:

xml
<available_skills>
  <skill>
    <name>...</name>
    <description>...</description>
    <location>file://...</location>
  </skill>
</available_skills>

这次抓包里 developer prompt 列出了 72 个 skill 描述,例如 bytedcli、lark-*、create-mr、update-mr、upload-figma-img-to-hera、verify-zenith-component、zenith-migration-workflow 等。

这里要区分 skills 和 tools:skills 是写在 developer prompt 文本里的能力路由说明,告诉模型遇到特定任务时应该加载哪个 skill;tools 是请求 JSON 顶层的工具 schema,这次有 68 个,模型可以直接通过 function_call 调用。

按长度粗略拆分,第一轮 developer prompt 是这样的:

text
OpenCode 身份和基础规则       948
Editing Approach              481
Autonomy and persistence      1090
Editing constraints           1968
Special user requests         904
Frontend tasks                699
Working with the user         3289
环境信息                       349
项目 AGENTS.md                3495
skills 列表                   26869
总计                          40238

所以,主 Agent 第一轮并不是只把用户问题和几个工具描述发给模型。它发的是一个“全局 Agent 运行说明 + 当前仓库项目说明 + skill 路由说明 + 工具 schema”的完整工作现场。

这一轮模型没有直接回答项目介绍,而是先发了一句进度说明:

text
我先快速看一下仓库根目录和关键配置,再结合项目说明给你概览。

随后它调用了 4 次本地 read 工具:

json
{
  "name": "read",
  "arguments": {
    "filePath": "/Users/bytedance/code/marketing_fe/package.json",
    "offset": 1,
    "limit": 200
  }
}
json
{
  "name": "read",
  "arguments": {
    "filePath": "/Users/bytedance/code/marketing_fe/pnpm-workspace.yaml",
    "offset": 1,
    "limit": 120
  }
}
json
{
  "name": "read",
  "arguments": {
    "filePath": "/Users/bytedance/code/marketing_fe/README.md",
    "offset": 1,
    "limit": 200
  }
}
json
{
  "name": "read",
  "arguments": {
    "filePath": "/Users/bytedance/code/marketing_fe",
    "offset": 0,
    "limit": 200
  }
}

这就是 Agent 的典型第一步:先读根目录、package.json、workspace 配置和 README,而不是凭空猜项目情况。

主 Agent 第二轮:工具结果被回灌到下一次 LLM 请求

本地工具调用不是模型自己执行的。模型输出 function_call 后,Agent runtime 在本地执行 read,再把结果组织进下一轮请求。

1163a2ca3cac64a628d6 就是第二轮请求。它的 Content-Length 约 156KB,比第一轮更大,因为它包含了上一轮的工具调用和工具返回内容。

这一轮 input_count 是 12,大致结构如下:

text
0  developer prompt
1  user 原始问题
2  上一轮 reasoning 加密内容
3  上一轮 assistant 文本
4  function_call: read package.json
5  function_call: read pnpm-workspace.yaml
6  function_call: read README.md
7  function_call: read 项目根目录
8  function_call_output: package.json 内容
9  function_call_output: pnpm-workspace.yaml 内容
10 function_call_output: README.md 内容
11 function_call_output: 根目录 listing

这很关键:每一轮模型请求并不是只发送「新消息」,而是把到目前为止的对话状态重新组织后发给模型。工具结果也会作为 function_call_output 回灌。

第一轮工具结果让模型知道了这些信息:

  • 项目名是 marketing-fe-monorepo。
  • 这是一个 private monorepo。
  • 依赖包含 React 18、React DOM、React Router、Universe Design、lodash-es 等。
  • README 说明项目基于 Eden Monorepo 管理。
  • 开发需要 Node >= 22,并配置 .aic/aic.env。
  • 根目录包含 sites/、infra/、platforms/、DEV.md、AGENTS.md、eden.monorepo.json 等。

第二轮模型依然没有最终回答,而是继续补充上下文。它先回复:

text
我已经确认这是一个内部 marketing 前端 monorepo,接下来重点补充目录分层、开发命令和技术栈。

然后继续调用 5 次 read:

text
/Users/bytedance/code/marketing_fe/DEV.md
/Users/bytedance/code/marketing_fe/eden.monorepo.json
/Users/bytedance/code/marketing_fe/sites
/Users/bytedance/code/marketing_fe/infra
/Users/bytedance/code/marketing_fe/platforms

从行为上看,模型已经形成了一个探索策略:先读项目入口文件,再读开发文档和关键目录结构,最后再总结。

主 Agent 第三轮:带着完整工具上下文生成最终回答

5ea0fffbb7377d2325f2 是第三轮请求。它的请求体约 173KB,input_count 达到 24。

这一轮包含前两轮所有内容:

  • developer prompt
  • 用户原始问题
  • 第一轮 assistant 文本
  • 第一轮 4 个工具调用
  • 第一轮 4 个工具结果
  • 第二轮 assistant 文本
  • 第二轮 5 个工具调用
  • 第二轮 5 个工具结果
  • 两轮 reasoning 的加密内容和摘要
  • 仍然携带 68 个工具定义

第二轮工具结果进一步补齐了信息:

  • DEV.md 说明仓库托管 Hera 平台开发者代码,主要包括组件、页面、模板、插件等。
  • Hera 登录命令是 pnpm aic hera login-hera -e cn|sg。
  • Zenith 登录命令是 pnpm aic login-zenith。
  • eden.monorepo.json 显示 pnpmVersion 为 10.12.1,edenMonoVersion 为 3.9.0。
  • sites/ 下有 activity/、feishu/、future/、lark/。
  • infra/ 下有 cli/、config/、integrations/、scripts/。
  • platforms/ 下有 hera-preview/ 和 rise/。

这一轮模型没有继续调用工具,而是直接生成最终回答。回答结构包括:

  • 项目概览
  • 主要技术栈
  • 核心目录
  • 业务模型
  • 典型开发流程
  • 常用命令
  • 开发规范重点

最终回答开头是:

markdown
**项目概览**

这是 `marketing_fe`,一个 ByteDance 内部的 Marketing 前端 monorepo,主要承载 Hera 平台上的官网页面、组件、模板、插件和相关开发工具。仓库使用 `emo` / Eden Monorepo 管理,底层是 `pnpm workspace`。

也就是说,最终回答不是模型凭先验知识生成的,而是基于两轮本地文件读取后的上下文总结。

SSE 响应里能看到什么

这些请求的响应都是 text/event-stream。从事件可以看到模型输出的全过程。

第一轮主 Agent 请求里,事件大致包括:

text
response.created
response.in_progress
response.output_item.added
response.reasoning_summary_text.delta
response.output_text.delta
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done
response.completed

当模型输出普通文本时,会出现 response.output_text.delta。当模型准备调用工具时,会出现 response.function_call_arguments.delta 和 response.function_call_arguments.done。

一次工具调用最终会被记录成类似这样的 output item:

json
{
  "type": "function_call",
  "name": "read",
  "status": "completed",
  "arguments": "{\"filePath\":\"/Users/bytedance/code/marketing_fe/README.md\",\"offset\":1,\"limit\":200}",
  "call_id": "call_IFW9e9Xfted1W4IQNFeC7vO4"
}

随后 Agent runtime 执行这个工具,并在下一轮请求里附上:

json
{
  "type": "function_call_output",
  "call_id": "call_IFW9e9Xfted1W4IQNFeC7vO4",
  "output": "<path>...README.md</path>\n<type>file</type>\n<content>..."
}

这个 call_id 把模型发起的工具调用和本地工具返回结果关联起来。

Token 和缓存观察

三轮主 Agent 请求的 token 使用量也很有代表性:

text
第一轮:input_tokens 29847,output_tokens 244,总计 30091
第二轮:input_tokens 35016,output_tokens 247,总计 35263
第三轮:input_tokens 39336,output_tokens 1002,总计 40338

第三轮里还能看到:

json
{
  "input_tokens_details": {
    "cached_tokens": 34816
  }
}

这说明随着多轮上下文增长,请求体会越来越大,但 prompt cache 能复用大量前缀 token。请求里也出现了:

json
{
  "prompt_cache_key": "ses_0a6a1de24ffeth9h5EOvPUAq4s"
}

这和 header 里的 x-session-affinity 指向同一个会话亲和标识,有助于服务端缓存和路由。

这次抓包揭示的 Agent 工作模式

从这次任务可以总结出一个典型 Agent loop:

  1. 用户输入任务。
  2. 系统额外发起一次轻量标题生成请求。
  3. 主 Agent 第一轮把 developer prompt、用户消息和工具定义发给 LLM。
  4. LLM 先回复进度说明,再输出工具调用。
  5. Agent runtime 在本地执行工具。
  6. 下一轮请求把历史消息、工具调用和工具结果一起发回 LLM。
  7. LLM 根据新上下文决定继续调用工具还是最终回答。
  8. 上下文足够后,LLM 输出最终总结。

这里最值得注意的是:LLM 本身并没有直接访问本地文件系统。它只是根据工具 schema 输出结构化的 function_call。真正读取文件的是 Agent runtime。读取结果再作为 function_call_output 放进下一轮 LLM 请求。

结论

一次看似简单的「介绍一下这个项目」,背后不是单次问答,而是多轮 Agent 编排。

标题生成请求只带标题 prompt 和用户原始输入,用小模型生成会话名。主任务请求则会带完整 Agent 指令、工具定义、历史消息、工具调用和工具结果。每轮 LLM 都在当前上下文下决定下一步:先读哪些文件、是否继续探索、什么时候可以总结。

这种机制的好处是回答更可验证:模型不是凭空介绍项目,而是先读项目文件,再基于实际内容总结。代价是请求体变大,工具 schema 和历史上下文会占用大量 token。因此 prompt cache、上下文裁剪和工具结果组织方式,都会直接影响 Agent 的成本、速度和回答质量。

评论
0/100