RussellLuo

让思想在文字间徜徉

引言

Claude Code伴侣(即Claude Code Mate,以下简称CCM)最初的定位是一个极简的LLM代理工具,旨在帮助开发者快速用上Claude Code和切换各种大模型。所以CCM一开始选择了纯命令行交互,以及通过YAML文件进行配置。这种方式既简洁高效,也符合开发者的使用习惯。

距离CCM第一版发布(轻松解锁Claude Code:国内用户的多元模型新玩法)已经过去了近一个月,随着自己使用的增多,以及零星收到的一些用户反馈,我发现CCM存在以下问题:

  1. 模型管理不直观:用户如果不查阅LiteLLM文档,很难知道CCM支持哪些模型,以及如何配置这些模型(比如OpenAI兼容的模型,需要加上openai/前缀)。
  2. 没有用量统计:用户在CCM中无法查看自己的用量情况,包括各个模型的请求数、输入/输出Token数和费用消耗等信息。

为了解决这些问题,我决定为CCM引入一个可视化管理后台,以提升用户体验。

PostgreSQL小插曲

了解LiteLLM的朋友可能知道,LiteLLM Proxy其实原本就提供Admin UI。然而,它对数据库的选择有特定偏好:仅支持PostgreSQL,并且明确表示不考虑SQLite等其他轻量级选项

这意味着,如果要启用Admin UI,用户必须要先安装和配置PostgreSQL。这显然与CCM的初衷——提供一个简单易用的工具——是相违背的。

幸运的是,经过一番调研,我找到了一个Python库pgserver,可以实现:

  • 可嵌入:通过pip安装依赖库,即可自动下载PostgreSQL
  • 零配置:无需用户手动设置数据库环境
  • 跨平台:支持Windows、macOS和Linux
  • 无Docker:不需要额外安装和配置Docker

于是在pgserver的帮助下,CCM成功引入了Admin UI,并做到了用户无感。

快速开始

为了保持轻量级,CCM默认不包含UI功能。如果要启用UI功能,可以通过以下命令安装:

1
2
3
4
# 使用uv(推荐)
uv pip install --system --python 3.12 "claude-code-mate[ui]"
# 或者使用pip
pip install "claude-code-mate[ui]"

安装后,使用以下命令启动UI:

1
ccm ui

打开Admin UI后,使用默认的用户名和密码(adminsk-1234567890)登录,即可进入管理后台。

可视化管理后台

LiteLLM Proxy提供的Admin UI功能很强大,其中就包括CCM第一版缺失的模型管理和用量统计功能。

模型管理

模型管理界面

LiteLLM内置支持众多提供商的模型,包括但不限于:

  • 知名官方模型(如Anthropic、OpenAI和DeepSeek等)
  • 聚合平台的模型(如OpenRouter等)
  • OpenAI兼容模型
  • Ollama本地模型

用户可以通过界面:

  • 轻松添加、编辑和删除模型,无需了解LiteLLM的特殊前缀规则。
  • 设置输入/输出的Token价格,以便准确计算费用。
  • 修改一些高级参数,如TPM/RPM、超时时间和max_tokens等。

用量统计

用量统计界面

通过用量统计功能,用户可以清晰地看到:

  • 总(或按模型)的请求次数,以及成功和失败的次数。
  • 总(或按模型)的Token数量,以及输入和输出的Token数量。
  • 总(或按模型)的费用消耗情况等。

其他功能

除了上述两个功能外,Admin UI还提供了以下一些实用功能:

  • 模型测试(Test Key):快速测试模型的可用性和效果。
  • 日志查看(Logs):实时查看请求日志,便于调试和排查问题。

模型测试界面

日志界面

以上只列举了Admin UI的部分功能,对于其他功能感兴趣的读者,可以进一步参考LiteLLM Proxy文档

结语

欢迎大家体验Claude Code Mate的新UI功能!需要说明的是,该功能只在macOS(我的开发环境)进行了测试,尚未在Windows和Linux上进行全面验证。如果你有任何问题或建议,欢迎随时在GitHub仓库中提出。

Learn about Claude Code, Anthropic’s agentic coding tool that lives in your terminal and helps you turn ideas into code faster than ever before.

Claude Code在官方文档中自称是一款Agentic(自主式,或智能体驱动)编程工具,它能够帮助开发者更高效地将想法转化为代码。那么,何为自主式编程?其背后的核心原理与技术实现又是怎样的?本文将围绕这一主题展开讨论,深入剖析Claude Code的技术架构与独到之处。

什么是自主式编程?

与传统基于静态提示词的单步代码生成工具不同,自主式编程系统以目标驱动、多步执行为核心特征。这些由大语言模型(LLM)驱动的系统能够自主执行软件开发任务,通过推理、决策、使用外部工具(如编译器、调试器、测试运行器)并基于反馈迭代优化输出结果。

自主式编程系统通常具有以下显著特点:

  • 高度自主性:能够在没有持续人工监督的情况下自主决策和执行操作
  • 环境交互性:在执行过程中与外部工具和环境进行动态交互
  • 迭代优化能力:基于中间反馈不断改进输出质量
  • 目标导向性:追求高层次的任务目标,而非仅仅对一次性指令进行响应

自主式编程代表了软件开发自动化的重要转变,从基于规则的自动化、传统机器学习模型或单次LLM调用,转向真正参与软件开发全过程的智能系统。这一变革为智能代码辅助、自主调试测试、自动化代码维护甚至自我进化的软件系统开辟了全新可能性。

Claude Code的核心架构

Claude Code自主式编程架构

如图所示,Claude Code作为典型的自主式编程工具,其核心架构包含以下几个关键组件:

  • 自主执行循环:Claude Code将大语言模型嵌入到一个执行循环中,使其能够与开发环境进行持续交互。当接收到用户的自然语言提示后,系统会从操作系统和工作区收集额外上下文信息(如文件摘要、环境状态等)。

  • 多步推理机制:在推理循环中,大语言模型会将复杂任务分解为多个子目标,生成代码或做出决策,并判断是否需要调用外部工具——包括文件读写、终端命令执行等操作。

  • 迭代优化过程:工具执行的结果会返回至循环中,作为进一步优化的反馈。这个迭代过程持续进行,直到自主系统完成任务或达到停止条件,最终结果将实时返回给用户。

案例分析:你好,世界!

为了更好地理解Claude Code的核心架构,让我们通过一个实际案例来观察其工作流程。

简单起见,我们来创建一个Python版的Hello World函数:

Hello World:输入提示词

Hello World:生成代码

Hello World:写入文件

从上述执行过程中,借助mitmproxy可以捕获到两条数据流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
================================================================================
FLOW #1 - ID: 6a662989-33d0-4e86-a984-8c39f90ed9e7
================================================================================

📤 REQUEST:
----------------------------------------
System:
System[0]:
type: text
text: You are Claude Code, Anthropic's official CLI for Claude.
System[1]:
type: text
text:
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.

...

Messages:
Message[0]:
role: user
content:
Content[0]:
type: text
text:
<system-reminder>
This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. If you are working on tasks that would benefit from a todo list please use the TodoWrite tool to create one. If not, please feel free to ignore. Again do not mention this message to the user.
</system-reminder>
Content[1]:
type: text
text:
<system-reminder>
As you answer the user's questions, you can use the following context:
# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.


IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
</system-reminder>

Content[2]:
type: text
text:
create a hello world function in python
Tools:
...

Tool[8]:
Name: Write
Description: Writes a file to the local filesystem.

Usage:
- This tool will overwrite the existing file if there is one at the provided path.
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
Input Schema:
Type: object
Required: file_path, content
Properties: file_path, content

...

📥 RESPONSE:
----------------------------------------
📡 Parsed SSE Response:
Tool: Write (ID: toolu_bdrk_01Tv5P8iPpKfvXkD7jGmjLFr) - {"file_path": "/Users/russellluo/Projects/demo/hello_world.py", "content": "def hello_world():\n print(\"Hello, World!\")\n\nif __name__ == \"__main__\":\n hello_world()"}

================================================================================
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
================================================================================
FLOW #2 - ID: 5b84e626-298d-4f35-a4c2-d22b1c0ab1ab
================================================================================

📤 REQUEST:
----------------------------------------
System:
...

Messages:
Message[0]:
role: user
content:
Content[0]:
type: text
text:
<system-reminder>
This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. If you are working on tasks that would benefit
from a todo list please use the TodoWrite tool to create one. If not, please feel free to ignore. Again do not mention this message to the user.
</system-reminder>
Content[1]:
type: text
text:
<system-reminder>
As you answer the user's questions, you can use the following context:
# important-instruction-reminders
Do what has been asked; nothing more, nothing less.
NEVER create files unless they're absolutely necessary for achieving your goal.
ALWAYS prefer editing an existing file to creating a new one.
NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.


IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
</system-reminder>

Content[2]:
type: text
text:
create a hello world function in python
Message[1]:
role: assistant
content:
Content[0]:
type: text
text:
(no content)
Content[1]:
type: tool_use
id: toolu_bdrk_01Tv5P8iPpKfvXkD7jGmjLFr
name: Write
input: {'file_path': '/Users/russellluo/Projects/demo/hello_world.py', 'content': 'def hello_world():\n print("Hello, World!")\n\nif __name__ == "__main__":\n hello_world()'}
Message[2]:
role: user
content:
Content[0]:
tool_use_id: toolu_bdrk_01Tv5P8iPpKfvXkD7jGmjLFr
type: tool_result
content: File created successfully at: /Users/russellluo/Projects/demo/hello_world.py
Tools:
...

📥 RESPONSE:
----------------------------------------
📡 Parsed SSE Response:
Created `hello_world.py` with a simple hello world function.

================================================================================

从数据流中可以看出,Claude Code处理“创建Hello World函数”这种简单任务,背后也是一个典型的多步、交互式的自主执行循环。其特点总结如下:

  1. 清晰的“思考-行动-观察”循环

    • 思考与行动(FLOW #1):这是循环的起点。LLM接收系统上下文和用户指令(create a hello world function in python)后,经过内部推理,决定采取行动——使用Write工具,它生成了完整的代码内容并指定了文件路径。
    • 观察与完成(FLOW #2):这是循环的下一步。经用户确认后,Write工具被执行,系统于是接收到了执行结果(File created successfully at...)。LLM观察到这个反馈后,判定任务已成功完成,于是决定结束循环,并生成最终响应给用户(Created 'hello_world.py' with a simple hello world function.)。
  2. 工具使用的链式依赖

    整个任务的完成依赖于工具的链式调用和结果传递:

    • tool_use(FLOW #1 响应中):LLM发起一个工具调用 (Write)。
    • tool_result(FLOW #2 请求中):环境执行工具后返回结果,并作为新的上下文输入给LLM。

    这表明自主系统的每一步决策都依赖于上一步执行后的环境状态,形成了一个闭环反馈。

  3. 状态保持与会话连续性

    • FLOW #2 的请求中包含了完整的对话历史(Messages[0], [1], [2]),其中包括最初的用户指令、LLM发起的工具调用以及工具执行的结果。
    • 这保证了LLM在每一步都有完整的上下文,能够理解当前任务所处的状态,从而做出连贯的决策。tool_use_id等字段确保了工具调用和结果之间的正确关联。

上述流程其实也是一个LLM工具使用的标准范式,感兴趣的读者可以参考Tool use with Claude

轻量级原型实现

通过前面的分析,我们已经深入了解了Claude Code的自主式编程原理。现在,让我们利用Anthropic Python SDK,构建一个轻量级的自主编程助手原型,将理论转化为实践。

下面的实现包含了简化的自主循环和Write工具,展示了如何让LLM突破纯文本生成的限制,真正与环境进行交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# Initialize Anthropic client
client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY"))

MODEL = "claude-sonnet-4"

# System prompt for Claude Code
SYSTEM = [
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.",
},
{
"type": "text",
"text": f"""
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.

Here is useful information about the environment you are running in:
<env>
Working directory: {CWD}
</env>
""",
},
]

# Write tool definition
WRITE_TOOL = {
"name": "Write",
"description": "Writes a file to the local filesystem.",
"input_schema": {
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "The absolute path to the file to write (must be absolute, not relative)",
},
"content": {
"type": "string",
"description": "The content to write to the file",
},
},
"required": ["file_path", "content"],
"additionalProperties": False,
},
}

def execute_tool(tool_name: str, tool_input: dict[str, Any]) -> str:
"""Execute tool function with user confirmation"""
if tool_name.lower() != "write":
return f"Error: Unknown tool `{tool_name}`"

# User confirmation
while True:
file_name = os.path.basename(tool_input["file_path"])
confirmation = (
input(f"""
```
{tool_input["content"]}
```
Do you want to create `{file_name}`? (Y/N) > """)
.strip()
.lower()
)
if confirmation == "y":
return write_file(tool_input["file_path"], tool_input["content"])
else:
return f"User rejected write to `{file_name}`"

def write_file(file_path: str, content: str) -> str:
"""Write content to a file at the specified path"""
try:
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
return f"File created successfully at: {file_path}"
except Exception as e:
return f"Error writing file: {str(e)}"

def run_agent_in_loop(messages: list[dict], max_turns: int = 5) -> None:
"""Run the coding agent in a loop"""
for turn in range(max_turns):
# Call Claude
response = client.messages.create(
model=MODEL,
max_tokens=1024,
system=SYSTEM,
tools=[WRITE_TOOL],
messages=messages,
)

response_content = response.content[0]

# Text response - task completed
if response_content.type == "text":
print(f"\n⏺ {response_content.text}")
break

# Tool call
elif response_content.type == "tool_use":
tool_use = response_content

# Execute tool
tool_result = execute_tool(tool_use.name, tool_use.input)

# Update message history
messages.extend(
[
{
"role": "assistant",
"content": [
{
"type": "tool_use",
"id": tool_use.id,
"name": tool_use.name,
"input": tool_use.input,
}
],
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": tool_result,
}
],
},
]
)

安装相关依赖,并设置API密钥:

1
2
pip install anthropic
export ANTHROPIC_API_KEY=<your_api_key>

运行上述代码lite_claude_code_v1.py完整版),输入指令create a hello world function in python,即可看到类似Claude Code的交互式执行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ python lite_claude_code_v1.py 
==================================================
🤖 Lite Claude Code (^C to exit)
==================================================

> create a hello world function in python

```
def hello_world():
print("Hello, World!")

if __name__ == "__main__":
hello_world()
```
Do you want to create `hello_world.py`? (Y/N) > y

⏺ I've created a simple Python file with a "Hello, World!" function.
...

>

至此,我们成功地构建了一个轻量级的Claude Code,并让它完成了一个基本的编程任务!感兴趣的读者可以进一步扩展这个原型,例如添加更多的工具(如ReadBash等),以增强其功能和实用性。

结语

通过本文的探索,我们剖析了Claude Code作为自主式编程工具的核心架构,揭示了其“目标驱动、多步执行”的工作原理。从简单的Hello World案例到背后的数据流分析,我们见证了其如何通过思考-行动-观察的自主循环,打通从自然语言指令到实际代码生成的完整路径。

此外,我们还构建了一个轻量级的自主编程助手原型,展示了如何利用Anthropic SDK,将复杂的交互逻辑简化为可执行的Python代码。这一过程不仅能加深我们对自主式编程的理解,也为未来开发自己的AI辅助工具奠定了基础。

当使用Claude Code这样的AI编程助手,在惊叹于它神奇能力的同时,你是否也曾好奇:它究竟是如何工作的?作为一款AI应用,Claude Code的魔法隐藏在它与大型语言模型(LLM)的每一次对话中。但遗憾的是,Claude Code是闭源软件,我们无法从源代码中寻找答案。

那么,如何才能揭开它的神秘面纱呢?突破口就藏在网络通信中!通过抓取和分析Claude Code与后端服务的网络流量,我们可以从中洞悉它的核心工作机制。今天,就让我们借助mitmproxy这个强大工具,开启这段技术探索之旅。

为什么需要网络分析?

在AI应用领域,提示词工程堪称核心机密。Claude Code的出色表现,既得益于Claude模型本身强大的能力,也离不开其精心设计的提示词模板和与LLM的交互策略。通过网络流量分析,我们可以:

  • 还原系统提示词(System Prompt)的设计
  • 分析不同任务对应的用户提示词(User Prompt)
  • 探索Agent的内部工具调用(Tools)
  • 研究上下文管理机制(Context Management)
  • 了解模型参数的配置细节和优化策略

这种“逆向工程”不仅能满足技术好奇心,更能为我们的AI应用开发提供宝贵参考。

mitmproxy:网络分析的瑞士军刀

mitmproxy是一个免费、开源的交互式HTTPS代理,可以用于拦截、检查、修改和重放网络流量。它提供了三个命令行/控制台工具:

  1. mitmproxy:一个基于ncurses的交互式工具,提供了一个带有快捷键的终端界面,允许用户实时查看和操作流量。这是最强大的交互模式。
  2. mitmdump:一个类似于tcpdump的命令行工具,不提供交互式界面,而是将捕获的流量直接输出到控制台或保存到文件。它非常适合与其它命令行工具(如grep,jq)结合使用,进行自动化日志记录和批量处理。
  3. mitmweb:一个基于Web的图形化界面,在浏览器中提供一个类似开发者工具的界面,对于不习惯命令行操作的用户来说更加友好。

对于Python开发者而言,安装mitmproxy只需一行命令(查看其他安装方式):

1
2
uvx mitmproxy
# or `pip install mitmproxy`

由于现代应用普遍使用HTTPS,我们还需要安装CA证书::

1
mitmproxy

运行后访问mitm.it,下载并安装对应系统的证书即可。

Claude Code流量初探

为了演示更直观,我们这里启动mitmweb:

1
mitmweb

mitmproxy默认监听8080端口(可以通过-p参数指定其他端口),因此需要将系统代理设置为该地址:

1
2
export http_proxy=http://127.0.0.1:8080
export https_proxy=http://127.0.0.1:8080

随后,我们可以选择一个项目,打开Claude Code并输入hi

claude-code-input-hi

如果一切顺利,mitmweb将会捕获到相关的网络流量:

claude-code-hi-flows-request

claude-code-hi-flows-response

至此,我们成功完成了对Claude Code的首次流量捕获!可以看到,这些流量包含了Claude Code与后端服务之间完整的HTTP请求和响应。在每一条数据流中,请求体包含了与提示词相关的输入信息,而响应体则包含了LLM生成的回答内容。

虽然请求和响应数据已被捕获,不过仔细观察我们会发现:

  1. 请求中的JSON数据内容较多,我们主要想分析与提示词有关的内容,如systemmessagestools等字段。
  2. 请求中的提示词往往是多行文本,都被显示在同一行,导致信息冗长且难以阅读。
  3. 响应中的SSE(Server-Sent Events)数据是以流的形式返回,导致它们被拆分成了多个片段,需要重新拼接起来。

为了解决上述几个问题,我们需要进一步使用mitmproxy的插件机制,来对流量进行定制化的捕获和解析。

mitmproxy插件机制

mitmproxy的插件机制基于事件驱动,其核心原理是:mitmproxy在代理流量的生命周期中,会在特定时刻(如收到请求、收到响应、发生错误等)触发预设好的事件。

开发者编写的插件(称为addon)实际上就是一个事件监听器。你通过创建一个类,并定义对应的事件处理方法(如request(self, flow)response(self, flow)),mitmproxy就会在相应事件发生时自动调用你的方法。

在这个方法内部,你可以通过操作flow对象(它包含了单个HTTP请求/响应的所有信息)来:

  1. 查看(Inspect):读取URL、头、内容等。
  2. 过滤(Filter):只处理你关心的特定流量。
  3. 修改(Modify):改变请求或响应的内容。
  4. 输出(Output):将信息打印到控制台或保存到文件。

这种机制让你能以非侵入的方式,对经过代理的流量进行精确的解析和处理。对编写插件感兴趣的读者,可以参考官方示例了解更多详情。

Claude Code流量的定制化分析

接下来,我们将借助mitmproxy的插件机制,对Claude Code的流量进行优化分析。

为了兼顾数据捕获的完整性和不同用户对格式化的个性化需求,我们将整个分析流程设计为两个独立的阶段:

  1. 流量记录:捕获所有HTTP请求和响应,并以JSON Lines格式逐行输出,确保数据的完整性,同时便于后续的逐行分析。
  2. 格式化输出:逐行解析流量记录,从JSON请求中提取systemmessagestools等核心字段,并以结构化的方式展示;同时将SSE响应数据流还原为完整的JSON对象,并格式化输出以提升可读性。

最有趣的是,在AI编程时代,我们甚至无需翻阅mitmproxy文档或手动编写代码——Claude Code就能帮我们完成大部分工作。

流量记录

首先,从官网下载示例io-write-flow-file.py。然后,打开Claude Code,让它参考示例生成mitmproxy插件:

参考io-write-flow-file.py,编写一个mitmproxy脚本,用于将请求/响应对写入一行JSON Line

claude-code-jsonl-writer-prompt

生成后的jsonl-writer.py插件(完整版)可以这样使用:

1
mitmweb -s jsonl-writer.py

运行后,所有捕获到的流量都会被记录到flows.jsonl文件中:

1
tail -f flows.jsonl

格式化输出

同样地,可以让Claude Code生成一个jsonl-viewer.py脚本,用于解析flows.jsonl文件中的每一行,并对其进行格式化输出:

编写一个解析脚本jsonl-viewer.py,可以逐行解析标准输入中的jsonl内容,每一行JSON Line格式如下:

1
{"request":{"model":"claude-3-5-haiku-20241022","max_tokens":512,"messages":[{"role":"user","content":"Please write a 5-10 word title for the following conversation:\n\n[Last 12 of 14 messages]\n\nUser: \n\nClaude: (no content)\n\nUser: \n\nClaude: (no content)\n\nUser: \n\nClaude: (no content)\n\nUser: \n\nClaude: I've created a mitmproxy addon `json_logger.py` that meets your requirements:\n\n## Key Features:\n\n1. **Request Body Filtering**: For JSON requests, only keeps `system`, `messages`, and `tools` fields\n2. **SSE Response Handling**: Parses Server-Sent Events and concatenates chunks into a structured format\n3. **JSON Lines Output**: Writes each request/response pair as a single JSON line to `requests_responses.jsonl`\n\n## Usage:\n\n```bash\n# Run with mitmproxy\nmitmdump -s json_logger.py\n\n# Or with mitmweb\nmitmweb -s json_logger.py\n```\n\nThe addon captures:\n- URL, HTTP method, and status code\n- Filtered request body (JSON fields: system, messages, tools)\n- Parsed response body (with special SSE chunk concatenation)\n- Content type information\n\nFor SSE responses, it extracts individual chunks and provides both the raw chunks array and concatenated content, making it easy to reconstruct streaming responses.\n\nRespond with the title for the conversation and nothing else."}],"system":[{"type":"text","text":"Summarize this coding conversation in under 50 characters.\nCapture the main task, key files, problems addressed, and current status.","cache_control":{"type":"ephemeral"}}],"temperature":0,"metadata":{"user_id":"user_f3bbbca5b09b9b5ebf3916e21fd50d585787d9b818164f839f3f9b9b4f58bfe2_account__session_08316681-f6ac-4fcb-80c6-142f1e956373"},"stream":true},"response":"event: message_start\ndata: {\"type\": \"message_start\", \"message\": {\"id\": \"msg_2d398cea-df7d-4792-8017-69fd8383b883\", \"type\": \"message\", \"role\": \"assistant\", \"content\": [], \"model\": \"anthropic/claude-3.5-haiku-20241022\", \"stop_reason\": null, \"stop_sequence\": null, \"usage\": {\"input_tokens\": 0, \"output_tokens\": 0}}}\n\nevent: content_block_start\ndata: {\"type\": \"content_block_start\", \"index\": 0, \"content_block\": {\"type\": \"text\", \"text\": \"\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"M\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"itmproxy JSON\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \" Logger for\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \" API\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \" Request\"}}\n\nevent: content_block_delta\ndata: {\"type\": \"content_block_delta\", \"index\": 0, \"delta\": {\"type\": \"text_delta\", \"text\": \"/Response Tracking\"}}\n\nevent: content_block_stop\ndata: {\"type\": \"content_block_stop\", \"index\": 0}\n\nevent: message_delta\ndata: {\"type\": \"message_delta\", \"delta\": {\"stop_reason\": \"end_turn\"}, \"usage\": {\"input_tokens\": 284, \"output_tokens\": 11}}\n\nevent: message_stop\ndata: {\"type\": \"message_stop\"}\n\n","flow_id":"75bbd359-8b0c-42ef-a376-fbd4c4660693","duration":2.0904617309570312}

解析要求:

  • 针对request数据,只提取system、messages和tools的内容,相关text和content字段包含多行文本,需要格式化输出
  • 针对response数据,需要将sse拼接成完整的json对象,然后其中的text字段包含多行文本,也需要格式化输出

claude-code-jsonl-viewer-prompt

生成后的jsonl-viewer.py脚本(完整版)可以用于解析flows.jsonl文件:

1
cat flows.jsonl | python jsonl-viewer.py | less

解析后的输出结果如下:

claude-code-jsonl-viewer-output

与原始的mitmweb界面相比,经过处理后的数据可读性有了显著提升:

  • 每条HTTP数据流独立呈现,请求和响应内容清晰分离
  • 提示词内容以整洁的多行文本格式展示,便于直观理解发送给LLM的具体指令
  • 响应中的SSE数据流被拼接为完整内容,更易于阅读

通过这种结构化的展示方式,每次AI对话的完整流程都变得清晰可追溯,大大提升了分析和调试的效率。

结语

通过本文的探索,我们见证了mitmproxy在流量捕获、插件定制与数据解析方面的卓越能力。同时结合Claude Code的AI辅助,我们可以高效编写和调试mitmproxy插件,为揭秘Claude Code自身的工作原理提供了强大的助力。

魔法的大门已然打开,而深处仍有更多奥秘。从提示词构造、Agent工具调用到上下文管理,Claude Code的核心机制,正静候每一位探索者的到来。

如果你对AI编程助手感兴趣,一定听说过Claude Code。遗憾的是,由于网络环境和账号限制,国内用户直接使用它非常困难。目前比较流行的方案是借助Claude Code Router,但经过笔者测试,配置并成功用起来仍有一定门槛。

作为Vibe Coding玩家,笔者尝试实现了一个简易版本的替代方案 —— Claude Code Mate,不仅能让你用上Claude Code,还能自由切换其他主流大模型。

有意思的是,Claude Code Mate项目本身的大部分代码,也是Vibe Coding的产物(由Claude Code生成)。

Claude Code Mate

项目地址:https://github.com/RussellLuo/claude-code-mate

Claude Code Mate(以下简称CCM)是一个专门为Claude Code设计的辅助工具,通过内置的LiteLLM Proxy,让你可以灵活接入多种大模型。它的最大特点是安装便捷、配置简单,适合不想折腾的开发者用户。

CCM能做什么

  1. 无障碍使用Claude Code

    无需直接注册Anthropic账号,只需配置一个第三方API Key(例如通过OpenRouter平台获取),即可在国内网络环境下稳定使用Claude Code。

  2. 一键切换多种模型

    除了Claude系列模型,你还可以方便地切换到Gemini、DeepSeek、GPT等其他大模型。只需在配置文件中简单设置,就能按需选用不同模型。

  3. 比同类方案更便捷

    其他流行方案如Claude Code Router(CCR)也能实现类似功能,但由于CCR有Router(路由器)和Transformer(转换器)等功能,导致理解和配置较复杂。相比之下,CCM只聚焦于切换不同的模型和提供商,并将所有功能打包成一条命令,安装后简单设置就能使用。

快速开始

以OpenRouter为例,只需几步即可完成:

1
2
3
4
5
6
7
8
9
10
11
12
pip install claude-code-mate

# 启动LiteLLM Proxy
export OPENROUTER_API_KEY=your-api-key
ccm start

# 设置环境变量(按`ccm start`的输出指示)
export ANTHROPIC_BASE_URL=http://0.0.0.0:4000
export ANTHROPIC_AUTH_TOKEN=sk-xxx

# 现在可以正常使用Claude Code了
claude --model claude-3.5-haiku

总结

Claude Code Mate的优势在于开箱即用和配置简便,特别适合以下用户:

  • 想用Claude Code但受限于网络环境的国内用户
  • 希望尝试不同大模型并集中管理配置
  • 喜欢简洁方案,不想花费太多时间进行其他定制

如果你正在寻找一个简单有效的方式使用Claude Code和切换各种大模型,不妨试试Claude Code Mate这个工具。

一、更新 John 的数据

假设我们有一条关于 John 的数据如下:

1
2
3
4
5
6
7
8
9
10
11
GET /people/1 HTTP/1.1

{
"name": "John",
"age": 20,
"address": {
"country": "China",
"province": "Guangdong",
"city": "Shenzhen",
}
}

现在我们想更新他的年龄和所在城市,于是发起了一个请求:

1
2
3
4
5
6
7
8
PATCH /people/1 HTTP/1.1

{
"age": 25,
"address": {
"city": "Guangzhou",
}
}

作为 Go 服务端开发人员,我们如何才能正确处理这个部分更新请求呢?

二、Go 零值与 JSON

乍一看并不难,我们立马写下了结构体定义:

1
2
3
4
5
6
7
8
9
10
11
type Address struct {
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}

JSON 反序列化?自然也不在话下:

1
2
3
4
blob := []byte(`{"age": 25, "address": {"city": "Guangzhou"}}`)
var person Person
_ = json.Unmarshal(blob, &person)
fmt.Printf("person: %+v\n", person)

对应的输出结果(Go Playground):

1
person: {Name: Age:25 Address:{Country: Province: City:Guangzhou}}

很显然,如果我们直接用 person 去更新 John 的数据,他的姓名、所在国家和省份都会被清空!

那服务端该如何正确识别客户端的原始意图呢?具体到 John 的例子,在 Go 中如何做到 “只更新他的年龄和所在城市” 呢?

三、业界通用解法

据我所知,对于上述问题,业界通常有以下三种解法。

使用指针

因为 Go 的零值特性,普通类型无法表达 “未初始化” 的状态,典型解法就是使用指针。

采用指针后,上面的结构体定义将变成:

1
2
3
4
5
6
7
8
9
10
11
type Address struct {
Country *string `json:"country"`
Province *string `json:"province"`
City *string `json:"city"`
}

type Person struct {
Name *string `json:"name"`
Age *int `json:"age"`
Address *Address `json:"address"`
}

再次进行 JSON 反序列化:

1
2
3
4
blob := []byte(`{"age": 25, "address": {"city": "Guangzhou"}}`)
var person Person
_ = json.Unmarshal(blob, &person)
fmt.Printf("person: %+v, address: %+v\n", person, person.Address)

对应的输出结果(Go Playground):

1
person: {Name:<nil> Age:0xc000018218 Address:0xc00000c138}, address: &{Country:<nil> Province:<nil> City:0xc0000103f0}

可以看到只有 Age 和 Address.City 的值不为 nil,于是我们只需要更新不为 nil 的字段即可:

1
2
3
4
5
6
7
8
9
10
11
func (a *Address) Update(other *Address) {
if other.Country != nil {
a.Country = other.Country
}
if other.Province != nil {
a.Province = other.Province
}
if other.City != nil {
a.City = other.City
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func (p *Person) Update(other *Person) {
if other.Name != nil {
p.Name = other.Name
}
if other.Age != nil {
p.Age = other.Age
}
if other.Address != nil {
// Side effects of using pointers
if p.Address == nil {
p.Address = new(Address)
}
p.Address.Update(other.Address)
}
}

参考完整代码(Go Playground)不难发现,使用指针后的 Person 结构体,操作起来会非常繁琐。比如:

  • 修改 address 前,需要首先保证 p.Address 不能为 nil
  • 此外,Initialization 初始化操作尤其麻烦

客户端维护的 FieldMask

受 Protocol Buffers 设计的影响,另一种较为流行的做法是在请求中新增一个 FieldMask 参数,用来补充说明需要更新的字段名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Address struct {
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}

type UpdatePersonRequest struct {
Person Person `json:"person"`
FieldMask string `json:"field_mask"`
}
1
2
3
4
blob := []byte(`{"person": {"age": 25, "address": {"city": "Guangzhou"}}, "field_mask": "age,address.city"}`)
var req UpdatePersonRequest
_ = json.Unmarshal(blob, &req)
fmt.Printf("req: %+v\n", req)

对应的输出结果(Go Playground):

1
req: {Person:{Name: Age:25 Address:{Country: Province: City:Guangzhou}} FieldMask:age,address.city}

有了 FieldMask 的补充说明,服务端就能正确进行部分更新了。但是对于客户端而言,FieldMask 其实是多余的,而且维护成本也不低(特别是待更新字段较多时),这也是我认为该方案最明显的一个不足之处。

改用 JSON Patch

前面讨论的方案,本质上都是 JSON Merge Patch 风格的。部分更新还有另外一个比较有名的风格,那就是 JSON Patch

具体到 John 的例子,部分更新请求变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PATCH /people/1 HTTP/1.1

[
{
"op": "replace",
"path": "/age",
"value": 25
},
{
"op": "replace",
"path": "/address/city",
"value": "Guangzhou"
}
]

相比于前面的解法而言,该解法的主要缺点是 PATCH 请求体跟待更新文档的 JSON 数据格式差异太大,表达上不太符合直觉。

四、服务端维护的 FieldMask

如果我们坚持 JSON Merge Patch 风格的部分更新,综合来看「客户端维护的 FieldMask」是相对较好的方案。那有没有可能进一步规避该方案的不足,即不增加客户端的维护成本呢?经过一段时间的研究和思考,我认为答案是肯定的。

有经验的读者可能会发现,Go 的 JSON 反序列化其实有两种:

  • 将 JSON 反序列化为结构体(优势:操作直观方便;不足:有零值问题)
  • 将 JSON 反序列化为 map[string]interface{}(优势:能够准确表达 JSON 中有无特定字段;不足:操作不够直观方便)

可想而知,如果我们直接把 Person 从结构体改为 map[string]interface{},操作体验可能会比使用带指针的结构体更糟糕!

那如果我们只是把 map[string]interface{} 作为一个反序列化的中间结果呢?比如:

  1. 首先将 JSON 反序列化为 map[string]interface{}
  2. 然后用 map[string]interface{} 来充当(服务端维护的)FieldMask
  3. 最后将 map[string]interface{} 解析为结构体(幸运的是,已经有现成的库 mapstructure 可以做到!)

通过一些探索和试验,结果表明上述想法是可行的。为此,我还专门开发了一个小巧的库 fieldmask,用来辅助实现基于该想法的部分更新。

具体到 John 的例子,借助 fieldmask 库,结构体可以定义成最自然的方式(不需要使用指针):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Address struct {
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
}

type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}

type UpdatePersonRequest struct {
Person
FieldMask fieldmask.FieldMask `json:"-"`
}

func (req *UpdatePersonRequest) UnmarshalJSON(b []byte) error {
if err := json.Unmarshal(b, &req.FieldMask); err != nil {
return err
}
return mapstructure.Decode(req.FieldMask, &req.Person)
}

注意,其中 JSON 反序列化的核心代码是 UnmarshalJSON。对应的更新逻辑如下(完整示例):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (a *Address) Update(other Address, fm fieldmask.FieldMask) {
if len(fm) == 0 {
// Clear the entire address.
*a = other
return
}

if fm.Has("country") {
a.Country = other.Country
}
if fm.Has("province") {
a.Province = other.Province
}
if fm.Has("city") {
a.City = other.City
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (p *Person) Update(other Person, fm fieldmask.FieldMask) {
if len(fm) == 0 {
// Clear the entire person.
*p = other
return
}

if fm.Has("name") {
p.Name = other.Name
}
if fm.Has("age") {
p.Age = other.Age
}
if addressFM, ok := fm.FieldMask("address"); ok {
p.Address.Update(other.Address, addressFM)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
john := Person{
Name: "John",
Age: 20,
Address: Address{
Country: "China",
Province: "Guangdong",
City: "Shenzhen",
},
}

blob := []byte(`{"age": 25, "address": {"city": "Guangzhou"}}`)
req := new(UpdatePersonRequest)
_ = json.Unmarshal(blob, req)

john.Update(req.Person, req.FieldMask)

个人觉得,相比其他方案而言,上述代码实现非常简单、自然(如果还有优化空间,欢迎指正👏🏻)。

当然该方案也不是完美的,目前来说,我认为至少有一个瑕疵就是需要两次解码:JSON -> map[string]interface{} -> 结构体,会增加一点性能上的开销。

五、相关阅读

0%