1. 为什么要抓 Claude 的包:从「黑盒对话」到「看得见的请求链路」
很多人用 Claude Code 或者 Claude API 的时候,心里其实没底:我发一句话,它到底往服务器发了什么?为什么有时候回答到一半断了?为什么明明本地有历史记录,翻出来却只有对话内容,看不到模型和工具之间来回调用的细节?
我自己最开始也是这个状态。.claude/project目录里确实有日志,看起来像传统 log,但仔细一看,里面只有用户和助手的对话文本,真正关键的「模型决策 → 调用工具 → 工具返回 → 再喂给模型」这条循环链路,全被[REDACTED]盖住了。日志里能看到类似这样的行:
2026-08-10T07:09:18.990Z [DEBUG] autocompact: tokens=[REDACTED] level=ok effectiveWindow=180000token 数被脱敏,工具调用的中间态也没暴露。也就是说,光靠客户端日志,你没法确认 Claude 是不是真的按你设想的「组装系统提示 + 用户指令 + 工具执行记录 + 文件内容 → 发 API → 收流式响应 → 再循环」在跑。
这时候抓包就是最直接的手段。Claude 的 API 走的是 HTTPS,请求体是 JSON,响应是 SSE(Server-Sent Events)流式分片。用 mitmproxy 做中间人,就能把请求头、请求体、每一个 chunk 都摊开看。这篇笔记聚焦的就是这条链路可视化:在 TaoToken 统一 Key / API 通道下,用 mitmproxy 抓取 Claude 请求与 SSE 流式响应,逐帧解析请求头、body 与 chunk 结构,并给出可复制的脚本、证书配置和过滤规则。
适合谁看?适合已经在用 Claude Code 或 Claude API、想搞清楚「它到底做了什么」的人;也适合想验证流式分片完整性、排查 401 / 代理失败 / 响应截断这类问题的同学。不需要你懂密码学,但需要你能在终端里跑命令、改环境变量。
核心检索词先摆出来:Claude 抓包、mitmproxy 流式响应解析、Claude API 请求全貌。这三个词贯穿全文,你按这个思路往下看就行。
抓包的目的不是「偷看」,而是验证和排障。验证的是:一次请求 = 用户问题 + 系统提示 + 工具/技能清单,一起发给 LLM;排障的是:当流式响应中断、当代理配置不生效、当返回 401 时,你能定位到是请求头没带对,还是证书没信任,还是代理变量被覆盖。
下面从环境准备开始,一步步来。
2. TaoToken 统一 Key 前置:把 Base URL、Key、Model ID 三件套先理顺
抓包之前,得先有一个稳定的 API 通道,否则你抓到的可能是一堆连接失败。这里我用 TaoToken 的统一 Key 通道来演示,原因是它把 Base URL 和 Key 的管理收敛到一处,抓包时请求头里的鉴权字段清晰,便于对照。
先把三件套说清楚,这是后面所有配置的基础:
| 项目 | 值 | 说明 |
|---|---|---|
| Base URL | https://taotoken.net/api | API 请求根地址,不带 UTM |
| API Key | 在控制台生成 | 形如sk-...,请求头里用 |
| Model ID | 例如claude-sonnet-4-5等 | 按你实际开通的模型填 |
控制台入口在这里:https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite
API Key 生成页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite
如果你还没决定用哪个模型,可以先在模型对话页试一下:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite
长期做编码或 Agent 任务的话,Coding Plan 更合适:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite
接入文档在:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite
Claude Code 专用接入说明:https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite
为什么强调「统一 Key」?因为抓包时你会看到请求头里带着鉴权信息。如果 Key 来源混乱,你分不清是哪个通道在发请求,排查 401 时就会绕圈。统一到一个 Key,请求头里的Authorization或x-api-key就是唯一标识,对照起来干净。
这里要提醒一句:抓包环境里不要把生产环境的 Key 直接暴露在共享终端里。mitmproxy 会把请求头完整记录下来,如果你把抓包文件发给别人,Key 就泄露了。建议单独生成一个测试用 Key,抓完就删。
配置 Claude Code 走 TaoToken 通道,核心是设置ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY(或对应的鉴权变量)。不同版本变量名可能略有差异,以接入文档为准。下面给一个通用的环境变量写法:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="sk-你的测试Key" export ANTHROPIC_MODEL="claude-sonnet-4-5"如果你用的是 Claude Code 的 settings 文件,可以写成 JSON。路径通常在~/.claude/settings.json或项目级.claude/settings.json:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的测试Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5" } }注意:这个 JSON 片段里的路径和字段名要和你本地实际文件一致,不要照抄字段名到不存在的文件里。改完用cat ~/.claude/settings.json确认一下。
三件套理顺之后,再启动 mitmproxy,抓到的请求才有意义。否则你抓到的可能只是「连接被拒绝」或者「证书错误」,看不到真正的请求体。
3. 可复制配置:mitmproxy 安装、证书信任与 Claude 走代理
这一节是全文最需要动手的部分。我按「安装 → 启动 → 证书 → 代理 → 过滤」的顺序来,每一步都给可复制的命令。
3.1 安装 mitmproxy
Ubuntu 22.04+ 可以直接用 apt,也可以用 pip 装最新版。pip 版本通常更新更快:
sudo apt update sudo apt install -y mitmproxy # 或者用 pip 安装最新版(推荐) pip3 install mitmproxy装完有三个命令行工具,用途不同:
mitmproxy:终端 TUI 界面,交互式查看流量,适合键盘操作。mitmweb:浏览器 Web 界面,默认在http://127.0.0.1:8081,更直观。mitmdump:纯命令行输出,适合重定向到文件或写脚本。
我平时用mitmweb看结构,用mitmdump配合脚本做过滤和落盘。
3.2 启动 mitmweb
# 终端 1:启动代理服务器,默认监听 8080 端口 mitmweb --listen-port 8080启动后浏览器会自动打开http://127.0.0.1:8081,这是流量查看面板,暂时是空的。
3.3 安装 mitmproxy CA 证书
HTTPS 流量是加密的,mitmproxy 必须作为中间人解密。第一次使用需要安装它的根证书:
# 先跑一下让证书生成 mitmdump & sleep 2 && kill %1 # 将 mitmproxy CA 证书添加到系统信任 sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy.crt sudo update-ca-certificates如果你用的是 Node 系工具(Claude Code 就是 Node 写的),系统证书信任还不够,Node 有自己的证书链。抓包时可以临时跳过证书验证:
export NODE_TLS_REJECT_UNAUTHORIZED=0注意:这个变量只建议在抓包调试时用,抓完就取消。长期开着会降低安全性。
3.4 让 Claude Code 走代理
先装 Claude Code 命令行版本:
# 初始化 package.json(如果还没有) npm init -y # 本地安装 Claude Code(不加 -g) npm install @anthropic-ai/claude-code # 安装完成后,命令行入口在: ./node_modules/.bin/claude然后配置代理。这里有个坑:系统或终端里可能已经有旧的代理变量,会覆盖你新设的。所以先彻底清除,再重新设置,大小写都设一遍:
# 1. 彻底清除所有旧代理变量 unset http_proxy HTTP_PROXY https_proxy HTTPS_PROXY all_proxy ALL_PROXY no_proxy NO_PROXY # 2. 重新正确设置(小写+大写都设,防止混淆) export http_proxy=http://127.0.0.1:8080 export https_proxy=http://127.0.0.1:8080 export HTTP_PROXY=http://127.0.0.1:8080 export HTTPS_PROXY=http://127.0.0.1:8080 # 3. 清除 no_proxy(否则某些域名会绕过代理) unset no_proxy NO_PROXY # 4. 验证 echo "https_proxy=$https_proxy" echo "HTTPS_PROXY=$HTTPS_PROXY" # 5. 测试 curl -v -k https://www.baidu.com 2>&1 | head -30如果curl能通,说明代理链路是活的。然后启动 Claude:
./node_modules/.bin/claude3.5 过滤规则:只看 Claude 相关流量
mitmproxy 默认抓所有流量,噪音很大。用过滤表达式只看目标域名。在mitmweb的 Filter 输入框里填:
~u taotoken\.net或者用mitmdump启动时直接带过滤:
mitmdump --listen-port 8080 -f "~u taotoken.net"如果你想把请求体和响应体落盘,写一个简单的 addon 脚本save_claude.py:
from mitmproxy import http import json import time def response(flow: http.HTTPFlow) -> None: if "taotoken.net" not in flow.request.pretty_host: return ts = time.strftime("%Y%m%d-%H%M%S") req_path = f"claude-req-{ts}.json" resp_path = f"claude-resp-{ts}.txt" with open(req_path, "w", encoding="utf-8") as f: f.write(flow.request.text or "") with open(resp_path, "w", encoding="utf-8") as f: f.write(flow.response.text or "") print(f"saved {req_path} / {resp_path}")启动:
mitmdump --listen-port 8080 -s save_claude.py这样每次 Claude 发请求,你都会得到一份请求 JSON 和一份响应文本,方便逐帧分析。
3.6 一个可复制的 settings 片段
如果你用 Claude Code 的 settings 文件,把代理和通道配置写在一起,路径以你本地为准:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的测试Key", "ANTHROPIC_MODEL": "claude-sonnet-4-5", "HTTP_PROXY": "http://127.0.0.1:8080", "HTTPS_PROXY": "http://127.0.0.1:8080", "NODE_TLS_REJECT_UNAUTHORIZED": "0" } }改完重启 Claude,触发一次对话,回到mitmweb面板,应该就能看到请求了。
4. 验证请求与流式响应:逐帧拆解 request body 和 SSE chunk
抓包成功后,最关键的是看懂两样东西:request 里的 JSON,和 response 里的 SSE 分片。
4.1 request 里到底装了什么
在mitmweb里点开一条POST /v1/messages请求,看 Request Body。你会看到类似这样的结构(篇幅原因只贴关键部分):
{ "model": "claude-sonnet-4-5", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is 2026-08-17.\n</system-reminder>\n\n你是谁?守则是什么?" } ] } ], "tools": [ { "name": "Bash", "description": "Run a shell command", "input_schema": { "type": "object", "properties": { "command": { "type": "string" } } } } ], "stream": true }这段 JSON 说明了几件事:
第一,用户原始问题(「你是谁?守则是什么?」)和系统注入的上下文(当前日期、Agent 类型说明、可用 Skills 说明)被打包在同一个messages数组里。系统提示不是单独字段,而是以<system-reminder>这种文本形式混在用户消息里。
第二,tools字段是工具定义的 JSON Schema 列表。这就是「把问题和工具都提供给 LLM」的含义——模型不是凭空知道有哪些工具,而是每次请求都带着工具清单。
第三,stream: true表示要流式返回。这就是为什么响应是 SSE 分片。
核心结论:一次请求 = 用户问题 + 系统提示 + 工具/技能清单,一起发给 LLM。抓包之前你可能只是「感觉」是这样,抓包之后是「看见」了。
4.2 response 的 SSE 分片结构
响应体是text/event-stream,每个事件由event:和data:两行组成。典型序列如下:
event: message_start data: {"type":"message_start","message":{"id":"msg_claude_188","type":"message","role":"assistant","content":[],"model":"claude-sonnet-4-5","stop_reason":null,"usage":{"input_tokens":0,"output_tokens":0}}} event: content_block_start data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"我是"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Claude"}} event: content_block_delta data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" Code"}} event: content_block_stop data: {"type":"content_block_stop","index":0} event: message_delta data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":42}} event: message_stop data: {"type":"message_stop"}逐帧看:
message_start:响应开始,带消息元数据和初始 usage,此时 token 数还是 0。content_block_start:文本块开始,index标识块序号。content_block_delta:一个个 token 增量返回。注意「我是」「 Claude」「 Code」是分开的,这就是打字机效果的来源。content_block_stop:文本块结束。message_delta:带stop_reason和最终 usage。message_stop:整个消息结束。
4.3 验证流式分片完整性的具体动作
怎么确认分片没丢?我一般做三件事:
第一,数content_block_delta的条数,和最终output_tokens对照。虽然 token 数和 delta 条数不是严格 1:1(一个 delta 可能含多个 token),但数量级应该对得上。如果 delta 只有几条而 output_tokens 是几百,说明中间被截断了。
第二,把每个text_delta的text字段按顺序拼接,看是否等于最终完整回答。写个小脚本:
import json deltas = [] with open("claude-resp-20260817-120000.txt", encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("data: "): payload = line[6:] try: obj = json.loads(payload) except json.JSONDecodeError: continue if obj.get("type") == "content_block_delta": deltas.append(obj["delta"].get("text", "")) full = "".join(deltas) print(f"delta count: {len(deltas)}") print(f"full text length: {len(full)}") print(full[:200])第三,检查是否有message_stop事件。如果响应在content_block_delta中途就断了,没有message_stop,那基本可以判定是网络或代理层截断,而不是模型主动结束。
4.4 用 mitmproxy 脚本实时统计分片
如果你想在抓包时实时看分片数,可以扩展前面的 addon:
from mitmproxy import http def response(flow: http.HTTPFlow) -> None: if "taotoken.net" not in flow.request.pretty_host: return body = flow.response.text or "" delta_count = body.count("content_block_delta") has_stop = "message_stop" in body print(f"[claude] deltas={delta_count} has_stop={has_stop} status={flow.response.status_code}")跑起来后,每完成一次请求,终端就会打印分片数和是否正常结束。这个动作对排查「回答到一半没了」特别有用。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
抓包过程中最容易卡在几个固定报错上。我按真实遇到的顺序列出来,对照着查。
5.1 401 Unauthorized
现象:请求发出去了,但响应是 401,body 里提示鉴权失败。
排查顺序:
先看请求头里有没有带 Key。在mitmweb里点开请求,看 Headers,找Authorization或x-api-key。如果为空,说明环境变量没生效。
再确认 Key 有没有多余空格或换行。从控制台复制时容易带上换行。用echo -n "$ANTHROPIC_API_KEY" | wc -c看长度是否符合预期。
最后确认 Base URL 和 Key 是配套的。如果你用 TaoToken 的 Key,Base URL 必须是https://taotoken.net/api,不能混用其他地址。
三件套再贴一次,方便对照:
- Base URL:
https://taotoken.net/api - Key:控制台生成,
sk-开头 - Model ID:按实际开通填
5.2 local proxy failed / 代理连接失败
现象:Claude 启动时报代理连接失败,或者curl测试不通。
原因通常是代理变量被覆盖,或者 mitmproxy 没在监听。
先确认 mitmproxy 在跑:ss -lntp | grep 8080,应该能看到监听。
再确认变量:env | grep -i proxy,看大小写是否都设了,有没有残留的no_proxy。
如果之前设过系统级代理,终端里的unset只影响当前会话。新开终端要重新设。
5.3 reading choices / 响应解析失败
现象:客户端报reading 'choices'之类的错误,通常是 OpenAI 格式和 Anthropic 格式混用导致的。
Claude 的 API 响应是 SSE,事件类型是message_start/content_block_delta,不是 OpenAI 的choices数组。如果你用了一个按 OpenAI 格式解析的客户端去接 Claude 通道,就会报这个错。
解决:确认客户端走的是 Anthropic Messages API 格式,Base URL 指向https://taotoken.net/api,不要指向 OpenAI 兼容端点(除非文档明确说明)。
5.4 OAuth / 登录态冲突
现象:Claude Code 提示 OAuth 相关错误,或者登录态和 API Key 冲突。
Claude Code 支持 OAuth 登录和 API Key 两种模式。如果你同时配了 OAuth 和ANTHROPIC_API_KEY,可能冲突。抓包场景下建议只用 API Key 模式,把 OAuth 相关配置清掉。
检查~/.claude/下有没有残留的凭据文件,必要时备份后移除,重新用 Key 登录。
5.5 证书错误
现象:SELF_SIGNED_CERT_IN_CHAIN或unable to verify the first certificate。
这是 Node 不信任 mitmproxy 证书。临时方案是NODE_TLS_REJECT_UNAUTHORIZED=0,长期方案是把 mitmproxy CA 证书导入 Node 的信任链。抓包调试用临时方案就够了。
5.6 抓不到包
如果mitmweb面板一直是空的,按这个顺序查:
- Claude 进程有没有继承代理变量?在启动 Claude 的同一个终端里
echo $https_proxy确认。 - 过滤规则是不是写错了?先去掉过滤,看有没有任何流量。
- 请求是不是走了别的域名?在面板里搜
taotoken或anthropic。 - 是不是用了 HTTP/2 或 QUIC?mitmproxy 对部分协议支持有限,可以在客户端强制 HTTP/1.1 试试。
排查完这些,基本能覆盖 90% 的抓包失败场景。
6. 把抓包变成日常习惯:从「看清一次」到「随时可查」
抓包这件事,做一次是好奇,做成习惯才是能力。我现在遇到 Claude 行为异常,第一反应不是猜,而是开 mitmproxy 抓一段。
几个实用技巧,都是踩过坑之后留下的:
第一,把抓包脚本和过滤规则存成文件,别每次手敲。我放在~/claude-debug/下,save_claude.py和start.sh各一份,需要时bash start.sh就起来。
第二,抓包文件按时间命名,方便回溯。前面脚本里的claude-req-{ts}.json就是这个思路。抓完一批,用grep -l "401" claude-resp-*.txt就能快速定位鉴权失败的请求。
第三,Key 用完就换。抓包文件里含完整请求头,测试 Key 抓完就删,别留在磁盘上。
第四,流式分片完整性检查脚本可以做成定时任务。如果你在跑长任务,隔一段时间检查一次有没有message_stop,能提前发现截断。
第五,把抓包结论写进项目笔记。比如「本次请求 tools 字段包含 Bash、Read、Edit 三个工具」,下次行为异常时对照,能快速判断是不是工具清单变了。
最后给一个日常排查的入口组合:排障和接入看 API Keys 和接入文档,验证模型行为去模型对话页,长期编码和 Agent 任务用 Coding Plan。链接都在前面第二节,按需取用。
抓包不是终点,看清链路之后,你对 Claude 的每一次调用都会更有把握。