RussellLuo

让思想在文字间徜徉

从ChatGPT的横空出世,到OpenClaw的一夜爆火,AI技术的发展可谓日新月异。如果说LLM是最强大脑,那么赋予它手和脚,使其从对话框中走出来的,正是Function Calling。

Function Calling(函数调用)有时也称为Tool Use(工具使用)。按照OpenAI的官方定义,Function Calling为LLM提供了一种强大且灵活的方式,使其能够与外部系统交互并获取训练数据之外的信息

要理解Function Calling的真正价值,我们需要先来了解其背后的应用场景。

应用场景

现实生活中,我们常常需要与各种系统和服务进行交互,以获取所需的信息或完成特定的任务。例如,查询天气、预订机票、支付账单、控制智能家居设备等。

以天气查询为例:

What is the weather like in Chengdu?

为了处理这个任务,AI系统通常需要执行以下操作:

  1. 理解用户的自然语言输入,并将其转化为结构化的指令或意图(这里的意图是“查询天气”,参数是“成都”)。
  2. 根据指令调用相应的API或服务,得到结果(这里的结果可能包括温度、湿度等)。
  3. 将结果加以整理和格式化,再以自然语言的形式返回给用户(例如,“今天成都的气温是22度,湿度为60%”)。

由此可见,这类AI系统的本质在于:

自然语言 → 结构化语义 → 触发系统动作

其中,从自然语言到结构化语义的转换,是整个流程的核心。

NLU

在LLM出现之前,这类任务通常由传统的自然语言理解(Natural Language Understanding, NLU)来完成。作为NLP的一个子集,NLU技术主要包括意图识别(Intent Recognition)和槽位填充(Slot Filling)两个子任务。

上述天气查询的例子,经过NLU处理后,得到的结构化输出大致是这样的:

query What is the weather like in Chengdu ?
slots O O O O O O B-loc O
intent get_weather

然而,NLU技术存在一些明显的局限性:

  • 所有意图必须预定义(分类)
  • 所有槽位必须事先建模(序列标注)
  • 泛化能力差,新场景需要重新训练模型
  • 多步推理能力几乎为0

Prompt Engineering

LLM兴起以后,其强大的生成式语义推理、结构约束和泛化能力,完美地解决了NLU的诸多痛点。然而,早期的LLM并不具备Function Calling的能力(如DeepSeek-R1)。于是,人们主要通过Prompt Engineering(提示词工程)的方式,引导LLM生成符合预期的结构化数据。

例如,针对天气查询的例子,可以设计如下Prompt:

You are an assistant that can perform actions based on user requests. Your responses should be in JSON format with the following structure:

1
2
3
4
5
6
7
{
"name": "action_name",
"arguments": {
"key1": "value1",
"key2": "value2"
}
}

Query: What is the weather like in Chengdu?

发给LLM后,就能得到如下JSON格式的文本输出:

1
2
3
4
5
6
{
"name": "get_weather",
"arguments": {
"location": "Chengdu"
}
}

Structured Output

上述方式虽然有效,但是稳定性不高。随着模型能力的演进,很多LLM开始原生地支持JSON mode,后来又进一步支持了Structured Output(结构化输出),生成结构化数据的能力得到了显著提升。

使用Structured Output,可以非常稳定地生成结构化数据。有些SDK(如OpenAI Python SDK),甚至还提供了与数据验证库(如Pydantic)的无缝集成,进一步增强了类型检查和数据验证的功能。例如:

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
from openai import OpenAI
from pydantic import BaseModel, Field

client = OpenAI()

class Arguments(BaseModel):
location: str

class Query(BaseModel):
name: str = Field(..., description="Action to perform")
arguments: Arguments = Field(..., description="Arguments for the action")

response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[
{
"role": "system",
"content": "Extract the query information.",
},
{
"role": "user",
"content": "What is the weather like in Chengdu?",
},
],
text_format=Query,
)

print(response.output_parsed)

# Output:
# name='getWeather' arguments=Arguments(location='Chengdu')

Structured Output vs Function Calling

事实上,在OpenAI的生态中,Function Calling(于2023年6月推出)先于Structured Output(于2024年8月推出)出现。

早期的Function Calling有时会“幻觉”出不符合格式的JSON,而Structured Output则可以确保输出严格遵循Schema。因此,Structured Output也可以看作是Function Calling能力的底层升级(Strict mode)。

从形式上来看,虽然Structured Output和Function Calling都会让LLM生成结构化的数据(通常是JSON),但它们解决的问题维度并不相同:

Structured Output Function Calling
主要目的 严格保证每一条输出都符合格式 由LLM灵活决定是否调用工具、调用哪个工具
适用场景 数据提取、实体识别、表单生成等 智能体(Agent)、检索增强(RAG)等

Function Calling

由上述分析可见,Function Calling的核心在于由LLM灵活决定是否调用工具、调用哪个工具。仍然以天气查询为例,如果我们将思维模式从“结构化输出”转变为“工具调用”,整体的解题路径就会截然不同。

Function Calling

如图所示,使用Function Calling来处理天气查询,整体的流程大致如下:

  1. 向模型发送用户请求,并明确声明其可调用的工具列表(如get_weather(location))。
  2. 模型根据请求,决定需要调用的工具名称(如get_weather)及相应的参数(如{"location": "chengdu"})。
  3. 应用程序解析工具调用请求后,执行对应的代码,并获取结果(如{"temperature": 14})。
  4. 应用程序携带工具调用的结果,再次向模型发起请求。
  5. 模型据此生成最终的回复(如It's currently 14°C in Chengdu.),或者再次调用其他工具。

如果觉得文字描述比较抽象,下面是一个可运行的Python示例:

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
import json
from openai import OpenAI

client = OpenAI()

# 1. Define a list of callable tools for the model
# (Note that `parameters` are defined in JSON Schema)
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City e.g. Beijng, Chegndu"
}
},
"required": ["location"],
"additionalProperties": False
},
"strict": True
},
]

def get_weather(location: str) -> str:
# Here you would normally make an API call to a weather service
return '{"temperature": 14}'

# Create a running input list we will add to over time
input_list = [
{"role": "user", "content": "What is the weather like in Chengdu?"},
]

# 2. Prompt the model with tools defined
response = client.responses.create(
model="gpt-5",
tools=tools,
input=input_list,
)

# Save function call outputs for subsequent requests
input_list += response.output

for item in response.output:
if item.type == "function_call":
if item.name == "get_weather":
# 3. Execute the function logic for `get_weather`
location = json.loads(item.arguments)["location"]
output = get_weather(location)

# 4. Provide function call results to the model
input_list.append({
"type": "function_call_output",
"call_id": item.call_id,
"output": output,
})

response = client.responses.create(
model="gpt-5",
instructions="You are a helpful assistant.",
tools=tools,
input=input_list,
)

# 5. The model should be able to give a response!
print(response.output_text)

# Output:
# The current temperature in Chengdu is about 14°C.

框架封装

至此,我们已了解了Function Calling的基本原理与使用方式。但在实际应用中,若每个工具都需要手动编写JSON Schema,其开发复杂度势必大幅增加。因此,众多开发框架对此进行了封装,以提供更高层次的抽象。

例如,使用LangChain提供的@tool装饰器,便能轻松地将Python函数注册为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
from langchain.chat_models import init_chat_model
from langchain.tools import tool

model = init_chat_model("gpt-5")

@tool
def get_weather(location: str) -> str:
"""Retrieves current weather for the given location."""
return f"It's sunny in {location}."

# 1. Bind (potentially multiple) tools to the model
model_with_tools = model.bind_tools([get_weather])

# 2. Model generates tool calls
messages = [{"role": "user", "content": "What's the weather like in Chengdu?"}]
ai_msg = model_with_tools.invoke(messages)
messages.append(ai_msg)

for tool_call in ai_msg.tool_calls:
# 3. Execute the tool with the generated arguments
tool_result = get_weather.invoke(tool_call)
# 4. Pass results back to model
messages.append(tool_result)

# 5. Model generates the final response
final_response = model_with_tools.invoke(messages)
print(final_response.text)

# Output:
# It's sunny in Chengdu right now.

如今,Agent已成为主流,上述工具执行循环(Tool Execution Loop)也演进为ReAct Agent的核心范式。为此,很多框架进一步对整个Agent模式进行了抽象,从而将Function Calling的复杂性封装在高层API之下,大幅降低了开发门槛。

例如,借助LangChain提供的create_agent函数,只需寥寥数行代码,就能构建出一个完整的天气查询Agent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from langchain.agents import create_agent
from langchain.chat_models import init_chat_model
from langchain.tools import tool

model = init_chat_model("gpt-5")

@tool
def get_weather(location: str) -> str:
"""Retrieves current weather for the given location."""
return f"It's sunny in {location}."

agent = create_agent(model, tools=[get_weather])
result = agent.invoke(
{"messages": [{"role": "user", "content": "What's the weather like in Chengdu?"}]}
)
print(result["messages"][-1].content)

# Output:
# It’s sunny in Chengdu right now.

结语

本文从实际应用场景出发,梳理了从传统NLU到Prompt Engineering,再到Structured Output和Function Calling的技术演进脉络,并对比了后两者在功能定位上的差异。最后,介绍了如何通过框架封装来简化Function Calling的开发流程。

在Claude Code、OpenClaw等智能体日益盛行的当下,Function Calling作为LLM的核心能力,已成为其重要的技术基石。随着这项技术的广泛应用,工具生态的开放性与标准化也愈发关键。在此背景下,MCP(模型上下文协议)已逐渐成为行业的事实标准,我们将在后续的文章中继续探讨这一话题。

自去年10月推出以来,Agent Skills迅速成为Claude Agent产品的核心组件,并在社区中得到了广泛的采纳和应用。关于Skills的介绍,网上已经有很多文章。本文尝试从底层原理的角度,探讨Agent Skills是如何工作的。

按照官方定义,Skills是一种基于文件系统的资源,用于为Agent提供特定领域的专业知识,从而将其转变为专家。理解Skills的前提,在于先厘清Agent的基本原理。

ReAct Agent

现代Agent都是基于ReAct模式构建的。ReAct的核心思想是将大语言模型的推理能力(Reasoning)与行动执行(Acting)相结合,使Agent能够反复思考问题、使用工具,并根据观察结果采取行动,从而实现用户目标。

ReAct模式

早期采用ReAct模式的Agent,效果并不理想。随着LLM能力的持续演进,特别是函数调用(Function Calling)的引入,ReAct模式的效果得到了显著改善,使得Agent能够更可靠且高效地完成任务。

以天气查询为例,从上下文(Context)的角度来看,ReAct Agent的运行过程大致如下:

1
2
3
4
5
System: You are a helpful weather assistant.
User: What is the weather like in Chengdu?
Assistant: ToolCall(name="get_weather", args={"location": "Chengdu"})
User: ToolOutput(result={"weather": "Sunny", "temperature": "22°C"})
Assistant: The weather in Chengdu is Sunny with a temperature of 22°C.

Claude Code

作为一个现代Agent系统,Claude Code也遵循了ReAct模式。我们在揭秘Claude Code:自主式编程中介绍过它的核心架构:

Claude Code自主式编程架构

可以看出,Claude Code与常规Agent(如天气查询助手)最大的不同之处在于:它工作在操作系统之上,几乎所有的工具都是围绕文件系统和Shell命令构建而成!

以“查看文件并创建一个Hello World函数”为例,Claude Code运行过程中的Context大致如下:

1
2
3
4
5
6
7
8
9
System: You are Claude Code, Anthropic's official CLI for Claude.
User: What files are there?
Assistant: ToolCall(name="Bash", args={"command": "ls"})
User: ToolOutput(result="[README.md]")
Assistant: There is only one file named README.md.
User: Create a hello world function in Python.
Assistant: ToolCall(name="Write", args={"file_path": "hello_world.py", "content": "def hello_world():\n print('Hello, World!')\n\nif __name__ == '__main__':\n hello_world()"})
User: ToolOutput(result="Created `hello_world.py` with a simple hello world function.")
Assistant: I've created a simple Python file with a "Hello, World!" function.

上下文管理

有了对ReAct Agent和Claude Code的基本认识,我们再来讨论一个关键话题——上下文管理。

了解大语言模型的读者可能知道,LLM的上下文有两个重要特征:

  1. 上下文窗口大小限制:LLM的上下文窗口大小是有限的(早期GPT 3仅有2048个token),虽然这个大小在持续增长(比如最新Claude Sonnet 4.5已支持百万token),但仍然是有上限的。
  2. 上下文过载导致性能下降:即使最先进的LLM支持长上下文(如百万token),但如果上下文内容过多,其性能也会显著下降。除了经典的Lost in the Middle,还会出现上下文污染(Context Poisoning)、上下文混淆(Context Confusion)等各种问题。感兴趣的读者可以进一步参考How Long Contexts Fail

因此,如何有效地管理上下文,成为了Agent设计中的一个重要课题。常见的上下文管理策略包括检索增强(RAG)、上下文总结(Context Summarization)、上下文隔离(Context Quarantine)和上下文卸载(Context Offloading)等。本文的讨论重点关注Context Offloading。

关于Context Offloading,How to Fix Your Context一文给出了以下定义:

上下文卸载(Context Offloading)是指将信息存到LLM的上下文之外,通常借助能管理数据的工具来实现。

而该文引用的Anthropic原文The “think” tool中,则这样指出:

这个“think”工具特别适合用在那些仅凭用户提问、Claude信息不够没法直接回答的情况,还有那些需要处理外部信息(比如工具返回的结果)的场景。比起深度思考那种全面推演,Claude用“think”工具做的推理会更聚焦,主要围绕模型刚发现的新信息展开。

在Claude Code中实现Context Offloading

上述关于Context Offloading的描述稍显抽象。为了便于理解,我们来设想一个问题:如果要让Claude Code支持Context Offloading,应该如何实现?

结合前文的讨论,我们知道Claude Code的能力基本建立在文件系统和Shell命令之上。因此,要实现Context Offloading,从第一性原理出发,我们能够很自然地想到以下方案:

  1. 将信息存储在文件系统,而不是一开始就全部放到LLM的上下文中;
  2. 为了让LLM知道这些信息的存在,需要在LLM的上下文中(通常在系统提示词中)记录这些信息的位置;
  3. 用户提问信息不全时,Agent可以根据系统提示词的引导,按需从文件系统寻找信息;
  4. 寻找信息的过程,需要借助文件读写和Shell命令等工具;
  5. 将获取到的外部信息加载到LLM的上下文中,以辅助完成用户的目标。

以“生成博客前端页面”为例,为了不把所有的设计指南都塞到LLM的上下文中,可以将其保存到resources/frontend-design.md文件中:

1
2
3
4
5
6
7
8
9
# Frontend Aesthetics Guidelines

Focus on:

**Typography**: Choose fonts that are beautiful, unique, and interesting...

**Color & Theme**: Commit to a cohesive aesthetic...

...

同时,在Agent的系统提示词中添加对这个文件的引用:

1
2
3
4
5
6
You are Claude Code...

You have access to the following resources:
- `resources/frontend-design.md`: Guidelines for designing the frontend UI.

...

如此一来,Agent只会在用户请求生成前端页面时,才会去额外读取resources/frontend-design.md文件中的内容,从而避免了不必要的上下文膨胀。具体来说,运行过程中的Context可能会是这样的:

1
2
3
4
5
6
7
8
9
System: You are Claude Code...\n\nYou have access to the following resources:\n- `resources/frontend-design.md`: Guidelines for designing the frontend UI.
User: Generate a blog frontend UI.
Assistant: ToolCall(name="Read", args={"file_path": "resources/fontend-design.md"})
User: ToolOutput(result="Choose fonts that are beautiful, unique, and interesting...")
Assistant: ToolCall(name="Write", args={"file_path": "app/index.html", "content": "<html><head><style>...</style></head><body>...</body></html>"})
User: ToolOutput(result="Created file `app/index.html`.")
Assistant: ToolCall(name="Write", args={"file_path": "app/styles.css", "content": "..."})
User: ToolOutput(result="Created file `app/styles.css`.")
Assistant: I've generated a simple blog frontend UI based on the guidelines.

讨论到这里,使用过Skills的读者可能发现了,如果把上述例子中的resources/重命名为skills/,那么frontend-design.md本质上就是一个Skill(参考anthropics/skills/frontend-design/SKILL.md)。

Skills的三层加载技术

至此我们可以看出,Skills的核心思想,其实也遵循了Context Offloading的上下文管理策略。当然,上述例子只是最基础的实现。

Agent Skills上下文窗口

在Anthropic的设计中,又巧妙地引入了Skills的三层加载技术,以求最大化减少LLM上下文的负担:

  1. 元数据(Metadata):可用Skills的名称、描述及其文件路径。这些信息会被预先放到上下文(系统提示词)中,以确保Agent知道有哪些Skills可以利用。
  2. 指令(Instructions):每个Skill都有一个对应的SKILL.md文件,其中包含了Skill的详细描述、使用方法和示例等信息。当Agent需要某个Skill的帮助时,它会通过Read工具读取SKILL.md文件的内容,进而将其动态加载到上下文中。
  3. 资源(Resources):除了SKILL.md文件,每个Skill还可以包含其他类型的资源文件,如配置文件、文档等。当Agent需要更具体的信息时,它会进一步读取这些资源文件的内容,从而将其加载到上下文中。

代码执行与虚拟机

除了前文讨论的内容,需要强调的是,Skills的完整能力还涉及代码执行和虚拟机:

  • 代码执行(Code Execution):某些Skills可能包含代码片段,甚至Agent为了处理任务还会动态生成代码,这些代码都需要执行。
  • 虚拟机(Virtual Machine):为了确保安全性,通常需要在一个隔离的沙盒环境(虚拟机)中管理文件系统、执行Shell命令和运行代码。

Agent Skills架构

由于篇幅所限,这里不再展开详细讨论,感兴趣的读者可以参考官方文档或者其他相关资料。

结语

通过本文的探讨,相信读者对Agent Skills有了更深入的理解。在Claude Agent产品中,Skills的实现基于Context Offloading这一上下文管理策略;而该策略的落地,则依托于ReAct模式的思想框架,以及文件系统、Shell命令等基础工具的支撑。

此外,代码执行和虚拟机也是非常重要的话题,本文限于篇幅只做了简要提及。实际上,它们不仅是Skills的关键技术,也代表着Agent未来的主流演进方向。在后续的文章中,我们将继续深入探讨这些话题,敬请期待!

引言

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的核心机制,正静候每一位探索者的到来。

0%