你的 AI Agent 到底发了什么请求,claude-tap 给它接了根监听线

发布于 2026年07月14日 08:51 #Agent 基建#Github 解读 原文链接

你的 AI Agent 到底发了什么请求,claude-tap 给它接了根监听线 封面图
  • 三档侵入度方案:reverse proxy、forward proxy、transcript 旁路,按客户端可观测接口从轻到重切换
  • WebSocket 强制降级为 HTTP/SSE,通过注入 supports_websockets=false 参数实现
  • 敏感头脱敏保留前缀可调试性,如 authorization 保留前 12 字符再截断
  • Bedrock SigV4 签名陷阱:不改写 base URL,改用 forward 模式透明转发
  • trace 压缩采用 blob 引用去重,viewer 核心是相邻请求结构化 diff

你写了个 prompt,Agent 跑了三分钟,改了五个文件,然后告诉你「搞定了」。你点头说好,但心里有个疑问始终悬着,它到底跟模型来回聊了什么,system prompt 长什么样,第几轮开始 token 暴涨,哪一次工具调用的参数明显跑偏了。

这些东西 Agent 自己不会告诉你,模型的 web 控制台也看不到,因为它记的是「调用次数」不是「调用内容」。你想到抓包,但 Wireshark 抓下来全是 TLS 加密的一堆乱码,Charles 要你配证书配代理配一晚上,最后发现 Agent 用的是 WebSocket 不是 HTTP,抓不全。

大家好,我是若风。今天拆的这个项目 claude-tap,干的就是这件事,给 AI 编码 Agent 接一根监听线,把每一次请求的完整内容,system prompt、对话历史、工具定义、流式响应、token 消耗,原样截下来,存成本地 trace,还能导出一个零依赖的 HTML 文件逐帧回放。

一句话定位

claude-tap 是一个本地代理 + trace viewer,目前覆盖 Claude Code、Codex CLI/App、Gemini CLI、Kimi CLI、Cursor CLI、OpenCode、Pi、Hermes Agent 等 14 个编码 Agent。MIT 协议,Python 写的,uv tool install claude-tap 一行装上,claude-tap 一行启动。

它不是又一个「LLM 网关」或「API 路由器」。Portkey、LiteLLM 那类项目是给你「统一调用入口 + 用量管理」的,流量过它们是为了被改写、被限流、被计费。claude-tap 不改写任何业务逻辑,它唯一的职责是「看见」,原样转发,原样记录,请求怎么发出去的就怎么到上游,响应怎么回来就怎么还给你的 Agent。

这个定位差异很重要,因为「观测」和「控制」是两件事。观测要求零侵入零改写,控制要求深度介入。claude-tap 选了前者,代价是它不能帮你做 fallback、负载均衡、模型路由,收益是你看到的 trace 就是真实发生的一切,没有被中间层美化或篡改。

为什么抓 Agent 的包这么难

你可能会想,抓个 HTTPS 包嘛,Charles 干了多少年了,有什么难的。难就难在 Agent 这个场景有三个反常识的坑。

第一个坑,Agent 的流量不止走一个域名。你以为 Claude Code 只跟 api.anthropic.com 说话,实际上 Codex 的 OAuth 模式走的是 chatgpt.com/backend-api/codex,Gemini CLI 的 Google OAuth 会分散到好几个 Google 域名,OpenCode 这种多 provider 的客户端更是你配了哪家就跑哪家。一个反向代理只认一个 upstream 的思路,到这里就抓不全了。

第二个坑,协议不止 HTTP/SSE。Codex 默认用 WebSocket 跟后端通信,WebSocket 是长连接双向的,传统的 HTTP 反向代理那套「请求来了转发响应」根本套不上,你得单独写一套 WS 帧的中转和录制逻辑。

第三个坑,有的流量根本不能碰。AWS Bedrock 的原生端点用 SigV4 签名,签名是跟请求的 host、path、headers 绑死的,你要是把 base URL 改成 localhost,签名立刻失效,请求直接被 AWS 拒掉。这时候你连反向代理的边都不能沾。

claude-tap 的整个架构,本质上就是在回答「面对这三种情况,分别该怎么观测」。它给出的答案是一套三档侵入度的方案。

三档侵入度,按需切换

这是我觉得这个项目最值得讲的设计,它没有一刀切地用某种代理方式,而是针对不同客户端的「可观测接口」分了三档。

第一档,reverse proxy(改环境变量)。这是最干净的方式,适用于那些「老老实实读 base URL 环境变量」的客户端,比如 Claude Code 读 ANTHROPIC_BASE_URL,Codex 读 OPENAI_BASE_URLclaude-tap 启动时在 cli_clients.py 里把这些环境变量指向本地代理端口,流量自然就拐过来了。看源码 ClientConfig 这个数据类,每个客户端都声明了自己的 base_url_envextra_base_url_envs,比如 Claude Code 除了主 ANTHROPIC_BASE_URL 还会检测 ANTHROPIC_BEDROCK_BASE_URLANTHROPIC_VERTEX_BASE_URL 两个变体。这种方式零证书、零系统配置,是首选。

第二档,forward proxy(注入 CA)。当客户端不听话,不暴露单一 base URL,或者像 OpenCode、Hermes 这种多 provider 的,你能配十家厂商它就跑十家,reverse 就抓不全了。这时候 claude-tap 降级到 forward 模式,给子进程注入 HTTPS_PROXY 和一个本地 CA 证书,所有 HTTPS 流量先经过 MITM 解密再转发。你看 forward_proxy.pyForwardProxyServer 这个类,它实现了完整的 CONNECT 隧道 + TLS 终结,客户端发 CONNECT api.anthropic.com:443,代理用自己的 CA 签一张临时证书递给客户端,解密后读到明文请求再加密转发给真上游。Gemini、Cursor、Qoder、Antigravity 这些默认都走这档。

第三档,transcript 旁路(不碰网络)。Codex App 这种带 GUI 的,根本不给你改环境变量的机会,它的会话直接落盘成 ~/.codex/sessions 下的 JSONL 文件。claude-tapcodexapp 模式就老老实实去 watch 这个目录,文件一更新就读进来,再尽力用 CDP(Chrome DevTools Protocol)补一点 WebSocket 证据。源码里 codex_app_transcript.py 干的就是这事,它是「观察者」不是「代理」,连网络都不碰。

这三档不是平行存在的备选,而是一套「侵入度阶梯」,能改环境变量就改环境变量,改不了就注入证书,证书都注入不了就退到读文件。这个思路我觉得挺有迁移价值的,后面收尾我会展开说。

系统架构图
系统架构图

上面这张图把整个项目从客户端接入到上游转发分成了四层。最上面 14 个客户端按各自的可观测接口归到三档代理模式,中间是三种流式协议(SSE / WebSocket / EventStream)的解码器,底层是 trace 存储、compact 去重和 diff viewer。顺着箭头从上往下看,你能感受到「侵入度」是怎么一档一档加上去的。

把 WebSocket 强制降级成 HTTP/SSE

前面提到 Codex 默认走 WebSocket,这是抓包的一个老大难。claude-tap 的处理方式很巧妙,它不去硬啃 WS 协议,而是让 Codex 自己降级。

cli_clients.py_codex_reverse_args 函数,这是给 Codex 拼启动参数的地方。关键就一行,f"{provider_key}.supports_websockets=false"。它会给 Codex 注入一个临时的 sibling provider 配置,把这个 provider 的 supports_websockets 标志强制设成 false,于是 Codex 就老老实实退回 HTTP/SSE 模式,一条请求一条 trace,干干净净。

而且它不碰你的 ~/.codex/config.toml,全部通过 -c 参数临时传入,跑完即弃。源码注释写得很直白,路由 Codex 经过代理走 HTTP/SSE,不改用户配置。这种「不改用户持久化配置」的纪律贯穿整个项目,后面讲 Bedrock 会再看到一次。

当然 WebSocket 不是完全不支持,ws_proxy.py 里有完整的 WS 帧中转逻辑,_handle_websocket 函数会同时维护客户端和上游两条 WS 连接,把每一帧的 payload 都记下来。只是对于 Codex 这种「能降级就降级更省事」的场景,强制降级是更聪明的选择。

敏感头脱敏,比你想得更细

抓包工具最容易被诟病的就是「会不会把我的 key 存下来」。claude-tapproxy.py 里有一个 SENSITIVE_HEADER_KEYS 集合,列出了所有要脱敏的头。

这个清单值得看一眼,因为它不是拍脑袋写的,是踩过坑的。除了常见的 authorizationx-api-keycookie,还有 x-amz-security-token(AWS 临时凭证),最特别的是一整组 cosy-keycosy-machinetokencosy-machineid 开头的头。注释解释说,Qoder/Cosy 运行时头会携带账号、机器、token 派生标识符,不能持久化进 trace。这说明作者确实接了 Qoder 这种国内厂商的客户端,而且被它的「机器指纹」头坑过,才会专门加进来。

脱敏的方式也不是一刀切抹成星号。PREFIX_REDACTED_HEADER_KEYS 这个集合里只放了 authorizationx-api-key,它们会保留前 12 个字符再截断,为什么,因为这俩头往往是 Bearer sk-ant-xxxsk-xxx 这种格式,保留前缀能让你在 trace 里分辨「这是哪种认证方式」,又不会泄露真正的密钥。其他敏感头直接变 ***。这种「脱敏但保留可调试性」的取舍,是做过运维的人才会有的手感。

Bedrock 的 SigV4 陷阱

这个细节最能体现作者对 AWS 的理解。AWS 原生 Bedrock 的请求是 SigV4 签名的,签名覆盖了 host、path、query、headers,你改任何一个字段签名就失效。

claude-tap 对 Bedrock 分了三种场景处理。第一种是 Anthropic 兼容的 Bedrock 网关(比如 New API 这种),没有 SigV4,正常反向代理即可。第二种是公司自建的 Bedrock 网关,也不是 AWS 原生域名,可以改 base URL。第三种是真正的 AWS 原生端点,域名带 *.amazonaws.com,这种情况 claude-tap 明确不会改写 ANTHROPIC_BEDROCK_BASE_URL 到 localhost,因为那样会破坏签名校验。

源码里看 ClientConfig 的逻辑,当检测到上游是 AWS 真域名时,它会提示你用 --tap-proxy-mode forward。forward 模式是做透明转发,CONNECT 隧道建立后原样把加密流量透传,不拆包不重签,虽然这样拿不到明文内容,但至少不破坏请求。这是一个诚实的妥协,作者没有为了「全都能抓」硬上 MITM 然后让 Bedrock 报错。

顺带一提,Bedrock 的响应是 AWS EventStream 二进制格式,不是普通 SSE。bedrock.py 里专门有 is_bedrock_eventstream_path 判断路径,_BEDROCK_MODEL_PATH_RE 用正则从 /model/{modelId}/invoke-with-response-stream 里抠出模型 ID,然后逐帧解码 EventStream。这部分代码不长,但全是跟 AWS 文档死磕的痕迹。

trace 怎么存,怎么变小

抓下来的数据量是惊人的。一次 Agent 会话动辄几十轮,每轮的 system prompt 可能就几千 token,tools 定义一堆 JSON,全存 JSONL 很快就几十 MB。claude-tapcompact_trace.py 给了一套自己的压缩方案。

它的思路是 blob 引用。超过 MIN_BLOB_BYTES(512 字节)的字段,比如 request.body.instructionsrequest.body.toolsrequest.body.messages 这些大块头,会被抽出来存进一个共享的 blobs 字典,记录里只放一个 __claude_tap_blob_ref__ 指针。如果两个请求用了同一份 system prompt(这在 Agent 会话里是常态,前 10 轮 system prompt 一字不差),它们就指向同一个 blob,去重了。

这套格式叫 compact trace,文件后缀 .ctap.json,是 claude-tap 的默认导出格式。它跟原始 JSONL 是可互转的,export 命令两种格式都能吃能吐。对于「想存档一次会话发给同事看」的场景,compact 格式 + 自包含 HTML 是最省事的组合。

viewer 的核心是 diff,不是展示

很多抓包工具的 viewer 就是「把 JSON 美化展示一下」,claude-tap 的 viewer 不一样,它的核心卖点是结构化 diff。

为什么 diff 这么重要,因为 Agent 调试的本质问题不是「这一次请求长什么样」,而是「这一轮相比上一轮多了什么、少了什么、改了什么」。你的 prompt 从第 5 轮开始 token 暴涨,你光看第 5 轮的请求看不出来,得跟第 4 轮 diff,才能发现是某个工具返回了一个巨大的文件内容塞进了上下文。

viewer_assets/diff.js 做的就是相邻请求的结构化对比,新增的消息、删除的消息、system prompt 的字符级差异,都用不同颜色标出来。配合 token usage 的 breakdown(input / output / cache read / cache creation 四项分列),你能精确看到每一轮的「上下文成本」是怎么累积的。

viewer 本身是一个零外部依赖的单 HTML 文件,所有 JS 都 inline 进去,CSS 也 inline。好处是导出后随便扔到哪都能打开,U 盘、邮件附件、内网 wiki,不用配环境。坏处是单文件会比较大,不过前面说的 compact 格式已经把体积压下来了。

老实说的几个限制

讲了一堆好的,也得说说它现在还不够好的地方,这些是我实际翻 issue 和源码发现的。

Windows 支持明显是二等公民。 issue #360 报的是 Windows 上启动直接 WinError 193,原因是 cli_clients.pyasyncio 的 subprocess 启动 claude,Windows 下 npm 装的 claude 是个 .cmd 脚本不是可执行文件,asyncio.create_subprocess_exec 拒绝执行。这个 issue 标着 bug 但还没合,说明 Windows 用户得自己绕路(比如手动指定 claude.cmd 路径或用 proxy-only 模式)。README 里大量示例都是 macOS/Linux 语感,Windows 的坑不少。

第三方供应商的兼容性是持续战斗。 issue #191 是评论区最热闹的(10 条评论),用户接阿里云 dashscope 报 502。这类问题的根源是各家「Anthropic 兼容 API」的兼容度参差不齐,有的不支持某个参数,有的 SSE 分帧方式不同。claude-tapproxy.py_normalize_request_body_for_upstream 里做了针对性修补,比如 Bedrock 网关会剥掉 Claude Code 的 beta-only 请求选项,但这种「打地鼠」式的兼容注定永远追不完。

原始流式事件默认不存。 这个是设计取舍不是 bug,但容易踩坑。默认 --tap-store-stream-events 是关的,trace 里只存重组后的完整响应,不存原始 SSE/WebSocket 帧数组。如果你要复盘「流式响应的时序」(比如分析首 token 延迟),必须启动时加这个 flag,事后补不了。这个默认值是为了省空间,但对「想看流式过程」的用户来说是个隐性陷阱,README 里提了一嘴但容易忽略。

多 provider 客户端 forward 模式要信任 CA。 OpenCode、Hermes、Gemini 这些走 forward 模式的,需要你在系统里信任 claude-tap 生成的本地 CA。macOS 上 Antigravity 甚至要往登录钥匙串里写,虽然不用 sudo 但会弹窗。对于公司电脑或对证书敏感的环境,这一步可能要过安全审批。作者在 certs.py 里把 CA 生成和信任的逻辑都封装好了,但「装个来路不明的 CA」这件事本身的信任成本,是用户要自己掂量的。

收尾,一套可迁移的观测侵入度阶梯

拆完 claude-tap,我想拎出来的不是某个具体技巧,而是它那套「三档侵入度」背后的一般方法论。

当你面对一个「想观测但不想破坏」的系统时,不要上来就选最重的方案(MITM 解密、改源码、插桩),而是按「目标系统暴露的可观测接口」从轻到重排个序。最轻的是「改配置让它经过我」(reverse proxy 改 env),次轻的是「在我的运行时里拦截它」(forward proxy 注入 CA),最重的是「它不配合我就旁路观察」(watch 文件、CDP 旁路)。能轻则轻,轻的搞不定再升级,每升一档都意味着更高的侵入度和更多的副作用。

这个思路不止适用于 AI Agent。微服务的 sidecar 观测、数据库的 query log、浏览器的 DevTools Protocol,底层都是同一个问题,怎么在不破坏目标行为的前提下把它「可见化」。claude-tap 把这个问题在「编码 Agent」这个具体场景里做透了,14 个客户端逐一分析它们的可观测接口,给出每个客户端最合适的档位,这套「侵入度阶梯 + 逐客户端适配」的组合拳,是它真正的工程价值。

如果你正在做 Agent 相关的开发,不管是调 prompt 还是排查行为异常,claude-tap 值得装一个备着。它不会帮你写更好的 prompt,但会让你看清你的 prompt 到底被怎么执行了。看清,是改进的第一步。

评论互动

© 2026 王若风的技术博客 · Powered by Astro