RussellLuo

让思想在文字间徜徉

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{} -> 结构体,会增加一点性能上的开销。

五、相关阅读

一、缘起

1. 背景:系统重构选型 Go kit

为什么不直接使用 Web 框架(如 Gin)?

根据旧系统使用 Flask 的经验,稍具规模、多人协作的工程代码,如果没有严格的分层约束,很容易做成一锅蛋炒饭(我们应该做盖浇饭)。

旧系统的常见问题:

  • HTTP 层和业务层,常常傻傻分不清楚(导致测试业务层代码,需要模拟 HTTP 请求);
  • HTTP/DB 层的细节,常常会混入业务层(比如业务层在处理 Flask 或者 SQLAlchemy 的数据结构);
  • 没有使用依赖注入,单元测试必须借助 Mock 工具(比如 Python 的 unittest.mock)。

为什么不使用 Go micro?

  • Go micro 作为一套微服务开发框架,复杂度较高,可控度不够;
  • 新系统一开始的定位是 “优雅(模块化)的大单体”(备注),所以选择了更灵活的 Go kit。

备注Microservice Architecture at MediumMedium 的微服务架构 )。

It is fine to start with a monolithic architecture, but make sure to modularize it and architect it with the above three microservice principles (single purpose, loose coupling and high cohesion), except that the “services” are implemented in the same tech stack, deployed together and run in the same process.

选择 Go kit 意味着什么?

  • 成熟的分层思想:整洁架构(意义指数:★★★)
  • 优雅的错误处理:如何将业务层 error 转换为 HTTP 状态码(意义指数:★★☆)
  • 灵活的协议切换:同时支持 HTTP 和 gRPC(意义指数:★★☆)

2. 问题:手写代码太繁琐

Go kit 核心代码分为三层:

业界吐槽:
Why I Recommend to Avoid Using the go-kit Library

3. 为什么要造轮子?

Go kit 列举的 两款工具

一言以蔽之:已有的轮子不简洁、不好用。

二、设计

1. 初衷

Kun 最初的定位:一款 HTTP 代码生成工具,让大家专注于 Service 层代码(业务逻辑),无需手写 Transport 层和 Endpoint 层代码。

Kun 可以生成的代码包括:

  • Endpoint 层代码
  • Transport 层代码
    • HTTP server
    • HTTP client
    • HTTP test
    • OAS 文档

2. 哲学

  • 聚焦最有价值的事
    • 别再折腾 Web 框架了,专注于你的业务价值!
  • 尽量只写 Go 代码
    • 用 Go(而不是其他 DSL)来描述你的服务定义。
  • 可维护性很重要
    • 拥抱整洁架构(核心是思想本质,而不是条条框框)。

3. 原理

参考 How it works

4. 注解

为什么要用注解?

选项 简洁度 可维护性
Go interface 上的直接注解 ★★☆ ★★★
单独的服务定义(如 YAML 文件) ★★☆ ★★☆(备注)

备注:「直接注解」的可维护性高于「YAML 文件」,源自 Where to configure transcoding

Two mechanisms are supported for doing this: direct annotations in your .proto file, and in YAML as part of your gRPC API configuration file. We recommend using proto annotations for ease of reading and maintenance.

注解设计

语法借鉴:

设计考量:

  1. 越典型的场景越简洁
    • 最典型的 JSON API 只需写一行 //kun:op
    • 支持自动绑定 URL 中的 Path 参数;
    • Query 也是常用的 HTTP 参数,所以 //kun:param 中默认 in=query,同时 //kun:body 中指定 - 可以让所有参数从 Body 切换绑定到 Query;
    • HTTP 参数名称默认为 snake case(下划线风格),也支持切换到 lower camel case(驼峰风格);
    • 200 是最常用的成功响应状态码,所以是 //kun:success 中默认 statusCode=200
  2. 复杂的场景也能支持
    • 需要使用 Query、Header、Cookie 参数时,可以用 //kun:param 来指定;
    • 支持参数聚合:将多个 HTTP 参数绑定到一个 Method 参数上;
    • 复杂请求和响应的编解码,可以通过自己实现 HTTP Codec 来达成。
  3. 服务接口完全由业务决定
    • Go interface 中每个 Method 的入参和出参,不强求一定要定义成结构体,可以从业务出发使用最自然的签名。

三、展望

Kun 未来的定位:不仅仅局限于一款 Go kit 的代码生成工具,而是成为一款通用的 Go 服务通信工具 —— 致力于处理 Go 服务之间的通信,让开发者专注于业务价值。

Kun 对服务通信的约定:

  1. 服务通信应该依赖于抽象接口,而不是具体类型;
    • 服务之间只依赖 Go interface;
    • 当前 Kun 生成的 HTTP client 代码也实现了服务定义的 Go interface。
  2. 基于约定 1,服务通信的类型应该是可以灵活切换的。
    • 最终 Kun 可能会支持的通信类型:
      • 进程内函数调用;
      • RPC(比如 HTTP 和 gRPC);
      • 异步消息(尚不支持)。
0%