Windows中使用WSL跑AI Agent

一、前言

最近开始大量使用 Codex、Claude Code、OpenCode 这类 AI Agent 之后,我看着Agent在我的windows上干活发现他会频繁地执行:

git status
find .
grep -R "xxx" .
cat package.json
npm install
python xxx.py
export API_KEY=xxxx
mkdir -p ./data
command1 && command2

但是总是会出现一些莫名其妙的报错:例如

引号不匹配
环境变量写法错误
路径格式错误
管道命令行为不一致
&& / || 行为不同
grep、sed、awk、find 不存在
export / source 无法使用

每次看着Agent出现这些错误都会被气死,不仅白吃我的token,而且还浪费了大量的时间。这并不是简单的“AI 不认识 PowerShell”。更准确地说,是目前大量 AI 编程工具、开源项目和开发工作流,都天然围绕 Unix/Linux 环境设计。Claude Code 官方文档明确把 Bash、Zsh、Fish 作为效果较好的 shell,并支持通过 WSL 在 Windows 上运行;OpenCode 官方也明确建议 Windows 用户使用 WSL,以获得更好的终端和开发工具兼容性。Codex CLI 的系统要求同样包含 Linux/macOS,并对 Windows 的使用提供了独立路径,其中 WSL2 是其文档中的 Linux 环境方案。

所以问题主要出现在:AI Agent 认为自己正在操作一个 Bash/Linux 环境,而你实际上给它准备的是 PowerShell。

这也是为什么我现在越来越倾向于一种 Windows + AI Agent 的工作方式:

Windows 负责桌面环境,WSL 负责 Linux 开发环境,AI Agent 直接运行在 WSL 里面。

这篇文章就记录一下,我是怎么把这一套环境搭起来的。


二、WSL 到底是什么

WSL(Windows Subsystem for Linux)

简单理解:
在 Windows 里面提供一个 Linux 用户空间和 Linux 开发环境。
它不是传统意义上的“再开一台 Linux 虚拟机”。你仍然可以:

Windows
 ├── 浏览器
 ├── 微信
 ├── VS Code
 ├── 文件管理器
 │
 └── WSL
      └── Ubuntu
           ├── bash
           ├── git
           ├── python
           ├── node
           ├── npm
           └── AI Agent

Microsoft 官方目前推荐直接使用:

wsl --install

进行安装。该命令会启用 WSL/虚拟机平台相关组件,并安装默认的 Ubuntu 发行版;安装完成后通常需要重新启动 Windows。

不过这条命令会把整个 Linux 环境装进系统盘。装到 D 盘能给 C 盘省下不少 Agent 跑出来的数据空间,做法见下一章。


三、安装 WSL 与文件系统

安装 WSL(并把它放到别的盘)

C:\Users\<你的用户名>\AppData\Local\Packages\<发行版包名>\LocalState\ext4.vhdx 这个 ext4.vhdx 就是整个 Linux 环境,装完工具、跑一段时间 Agent,几十个 G 很正常。使用 wsl --install 默认是安装在系统盘。我这台机器上现在 WSL 相关服务使用磁盘用量是 19.88 GB,全放 C 盘属实有点顶。如果想放在指定的路径,各位可以参考这个命令安装,D:\DevTools\WSL\Ubuntu24 这个路径按照自己的习惯切换即可:

wsl --install Ubuntu-24.04 --location D:\DevTools\WSL\Ubuntu24 --name Ubuntu24

然后按照提示重启电脑。
重启后,第一次启动 Ubuntu 时,系统会让你创建一个 Linux 用户名和密码。注意:这里的 Linux 用户和 Windows 用户不是一回事,用户名随便填都可以,但不能填 root,那是系统保留账户,会被直接拒绝。

本文后面统一以 root 用户为例,家目录是 /root。想和我保持一致的话,先随便建一个用户,进入 WSL 后设置 root 密码并把默认登录用户切成 root:

sudo passwd root
echo -e "[user]\ndefault=root" | sudo tee -a /etc/wsl.conf

回到 PowerShell 执行 wsl --shutdown,再次进入 WSL 就是 root 了,用 whoami 确认。

如果你保留自己创建的普通用户,后文所有 /root/ 换成 /home/<你的用户名>/ 即可。

最后用 wsl -l -v 确认装的是 WSL 2,VERSION 那一列必须是 2。显示 1 的话手动转换,并把 WSL 2 设为以后的默认版本:

wsl --set-version <你的发行版名> 2
wsl --set-default-version 2

两套文件系统:Windows 和 WSL 怎么互通

WSL 自己有一套 Linux 文件系统,/root/home/<你的用户名>/usr/etc 这些就是 Linux 环境本身,项目、Node.js、Python、Git、AI Agent 都可以直接放在这里。

同时 WSL 默认也能直接访问 Windows 文件,Windows 磁盘会自动挂载到 /mnt/ 下,C:\ 对应 /mnt/c/D:\ 对应 /mnt/d/,依此类推。所以 C:\Users\<你的Windows用户名>\Desktop\demo 在 WSL 里就是 /mnt/c/Users/<你的Windows用户名>/Desktop/demo,可以直接 cd 进去。

反过来,Windows 资源管理器地址栏输入 \\wsl$ 就能访问 WSL 的 Linux 文件系统。两边的文件可以直接互相访问。

项目放在哪里

为什么我不建议放 /mnt/c

这是 WSL 新手最容易踩的坑之一。cd /mnt/c/Users/<你的Windows用户名>/Desktop/project 当然能跑,但对于 Node.js、Python、Git、node_modules、虚拟环境、大型代码仓库、AI Agent 这类涉及大量小文件读写的工作负载,跨文件系统的性能损失比较明显。

我更推荐把代码放进 WSL 自己的 Linux 文件系统:

mkdir -p /root/AI_Workspace
cd /root/AI_Workspace
git clone https://github.com/xxx/xxx.git

这样项目就落在 /root/AI_Workspace/root 是 root 用户的家目录,用自己创建的普通用户则是 /home/<你的用户名>/AI_Workspace

那 Windows 文件怎么办

不用担心,WSL 仍然可以直接访问 Windows 文件,cd /mnt/c/Users/<你的Windows用户名>/Downloads 或者复制进来都行:

cp /mnt/c/Users/<你的Windows用户名>/Desktop/test.py /root/AI_Workspace/

反过来,Windows 文件管理器地址栏输入 \\wsl$ 就能看到 WSL 中的发行版。

最终可以这样理解

Windows
│
├── C:\ / D:\ / E:\
│       ↓
│     /mnt/c / /mnt/d / /mnt/e
│
└── WSL
     │
     └── Linux 文件系统
          ├── /root
          ├── /home
          ├── /usr
          ├── /etc
          └── /root/AI_Workspace

所以要记住:WSL 默认能访问 Windows 磁盘,不代表 WSL 的 Linux 文件系统必须放在 C 盘。 安装位置和 /mnt/c 是两回事,前者决定 WSL 的 Linux 环境存在哪里,后者只是 WSL 访问 Windows 文件的挂载入口。


四、WSL 网络配置

这一章是我踩坑最多的地方,所以写得细一点。WSL 的网络如果没配好,后续使用会遇到很多莫名其妙的问题,装 Node、uv、AI Agent 都要走网络,先把这里配通,不然每一步都会卡。

先搞清楚自己在哪个模式

WSL 2 有两种网络模式分别是NATMirrored代理配置方式完全不同,配之前必须先确认自己在哪个模式。

1. 两种模式的本质区别

NAT(默认)

WSL 拿到一个独立网段,通过 Windows 上的虚拟网卡做 NAT 出网。它和 Windows 是两台“机器”。

Windows 主机  192.168.x.x(你的 WiFi)
     │
     └── vEthernet (WSL)  172.18.48.1   ← WSL 眼里的网关
              │
              └── WSL eth0  172.18.59.131/20

关键一点:

WSL 里的 127.0.0.1 是 WSL 自己,不是 Windows。

所以 Windows 上监听 127.0.0.1:7897 的代理软件(例如Clash Verge),在 WSL 里怎么都连不上。这是绝大多数“WSL 代理配了但没用”的根因。

Mirrored(镜像,Windows 11 22H2+)

WSL 直接共用 Windows 的网络栈,网卡、IP、loopback 全部一致。这个模式对 VPN、IPv6、localhost 互访、局域网访问的兼容性明显更好,代理配置也简单得多。

2. 确认当前模式

先看配置文件,路径在 C:\Users\<你的用户名>\.wslconfig。没有这个文件,或者文件里没写 networkingMode,就是默认的 NAT。

更可靠的方式是在 WSL 里直接看网卡:

ip -brief addr
ip route show default

NAT 模式会看到 eth0172.18.x.x/20 这样的私有地址,默认路由指向 172.18.x.1Mirrored 模式会看到和 Windows 那边一样的网卡和 IP(比如你的 WiFi 地址),并且多出一个 loopback0 接口。

3. NAT 模式的基础配置

如果你不打算换 mirrored,NAT 下建议至少把 DNS 隧道打开。编辑 C:\Users\<你的用户名>\.wslconfig

[wsl2]
networkingMode=NAT
localhostForwarding=true
dnsTunneling=true
autoProxy=false

dnsTunneling=true 让 WSL 的 DNS 查询通过隧道交给 Windows 解析器处理,在 VPN、公司网络这类环境下比 WSL 自己生成 /etc/resolv.conf 稳得多。

autoProxy=false 表示不自动继承 Windows 的系统代理设置。NAT 模式下我不建议依赖它——如果你的代理只监听 127.0.0.1,继承过来的地址在 WSL 里照样连不通,反而多一层不确定性。NAT 下用下面「NAT 模式下的代理配置」的显式写法,行为完全可控。启动时如果看到 检测到 localhost 代理配置,但未镜像到 WSL 这行警告,说的就是这件事,那一节末尾专门讲了怎么处理。

改完 .wslconfig 必须执行 wsl --shutdown 重启 WSL 虚拟机才生效,然后重新打开终端。注意这个命令会关掉所有发行版和正在跑的进程,先存好手上的活。

先测试基础网络,再决定要不要配代理

这里我特别建议:先测试基础网络,再决定要不要配置代理。

在 WSL 的终端里先测 DNS 和直连:

getent hosts github.com
curl -I https://github.com
curl -I https://registry.npmjs.org

如果这几个都通了,说明 WSL 的基础网络没问题,你只需要针对被墙的服务配代理,不用全局改。

最后用这条命令看一下自己的实际出口,输出里的 ip=loc= 就是真实的出网地址和地区:

curl https://www.cloudflare.com/cdn-cgi/trace

这个命令比 curl -I https://www.google.com 有用得多——它能告诉你代理到底有没有生效。后面每次改完代理配置,都用它验证。否则一旦一开始就在:Windows 代理 → PowerShell → WSL → Bash → npm → AI Agent中间叠加五六层配置,后面排查会非常痛苦。

NAT 模式下的代理配置

NAT 模式下要让 WSL 用上 Windows 的代理(以Clash Verge为例,我的电脑的Clash Verge端口是默认的7897,各位根据自己实际的端口运行命令进行修改),需要三步。这三步缺任何一步都不通,而第二步是最容易被漏掉的。

第一步:代理软件开放局域网连接

因为 WSL 在另一个网段,代理软件必须监听 0.0.0.0 而不是只监听 127.0.0.1

Clash / Clash Verge / mihomo 在设置里打开「局域网连接」(对应配置项 allow-lan: true),其他工具找 Allow LAN / 允许局域网 / Listen on all interfaces 之类的选项。

第二步:Windows 防火墙放行端口

这一步最容易漏。allow-lan 开了但 Windows 防火墙默认拦截入站连接,WSL 连过去还是超时。以管理员身份打开 PowerShell 执行:

New-NetFirewallRule -DisplayName "Proxy for WSL 7897" -Direction Inbound -Action Allow -Protocol TCP -LocalPort 7897 -RemoteAddress 172.16.0.0/12 -Profile Any

-RemoteAddress 172.16.0.0/12 只放通 WSL 所在的私有网段,别图省事写 Any——那等于把代理端口暴露给你连过的每一个 WiFi。想删掉这条规则用 Remove-NetFirewallRule -DisplayName "Proxy for WSL 7897"

第三步:WSL 里配置代理变量

NAT 模式有个麻烦事:网关 IP 每次重启 WSL 都可能变。所以不要写死 IP,从路由表动态取。

执行 sudo nano /etc/profile.d/proxy.sh 新建文件,内容如下:

bash
# WSL2 (NAT) -> Windows 主机上的代理
# 网关 IP 每次重启会变,所以从路由表动态读取
__proxy_gateway() {
    ip route show default 2>/dev/null | awk '/default/ {print $3; exit}'
}

# 启动时探测一次,代理没开就不设变量,避免留下一个连不通的死代理
__proxy_reachable() {
    timeout 1 bash -c "exec 3<>/dev/tcp/$1/$2" 2>/dev/null
}

proxy_on() {
    local gw port=7897
    gw=$(__proxy_gateway)
    if [ -z "$gw" ]; then
        echo "proxy_on: 找不到默认网关" >&2
        return 1
    fi
    export http_proxy="http://${gw}:${port}"
    export https_proxy="$http_proxy"
    export all_proxy="socks5h://${gw}:${port}"
    export no_proxy='localhost,127.0.0.1,::1,172.16.0.0/12,192.168.0.0/16,10.0.0.0/8,*.local'
    export HTTP_PROXY="$http_proxy"
    export HTTPS_PROXY="$https_proxy"
    export ALL_PROXY="$all_proxy"
    export NO_PROXY="$no_proxy"
    echo "proxy on -> ${gw}:${port}"
}

proxy_off() {
    unset http_proxy https_proxy all_proxy HTTP_PROXY HTTPS_PROXY ALL_PROXY
    export no_proxy='localhost,127.0.0.1,::1'
    export NO_PROXY="$no_proxy"
    echo "proxy off"
}

proxy_status() {
    echo "http_proxy=${http_proxy:-<未设置>}"
    printf '实际出口: '
    curl -sS -m 12 https://www.cloudflare.com/cdn-cgi/trace 2>/dev/null \
        | awk -F= '/^ip=/{ip=$2} /^loc=/{loc=$2} END{if (ip) print ip" ("loc")"; else print "不可达"}'
}

# 代理端口真的在监听时才自动开启
if [ -z "${PROXY_AUTO_OFF:-}" ]; then
    __gw=$(__proxy_gateway)
    if [ -n "$__gw" ] && __proxy_reachable "$__gw" 7897; then
        proxy_on >/dev/null
    fi
    unset __gw
fi

/etc/profile.d/ 只被登录 shell 加载,所以在 /root/.bashrc 末尾再 source 一次,保证非登录 shell(VS Code 集成终端、bash -c 之类)也生效:

if [ -f /etc/profile.d/proxy.sh ]; then
    . /etc/profile.d/proxy.sh
fi

验证

重开 WSL 终端执行 proxy_status,正常会打印 http_proxy=http://172.18.48.1:7897 和变化后的出口地区。再对比一下开关效果,确认代理真的在起作用:

proxy_off && curl -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc)='
proxy_on  && curl -s https://www.cloudflare.com/cdn-cgi/trace | grep -E '^(ip|loc)='

两次输出的 iploc 应该不一样。如果一样,说明代理没生效。

一个典型的失败例子

启动 WSL 的时候如果看到检测到 localhost 代理配置,但未镜像到 WSL。NAT 模式下的 WSL 不支持 localhost 代理。这句警告,说明你正好撞上了本章开头讲的那个坑。这不是报错,是 WSL 在提醒你「你的代理配置在 WSL 里用不了」。 它由 wslservice.exe 在启动时发出,只出现在 NAT 模式下。

原因就是前面说过的那件事:Windows 系统代理设置里写的是 127.0.0.1:7897,而 NAT 模式下 WSL 是一台独立的虚拟机,它的 127.0.0.1 指向自己,不是 Windows。所以这个地址搬到 WSL 里没有任何意义 —— WSL 检测到了这个配置,也知道自己没办法用,就打了这行提示。

这行警告本身不影响 WSL 启动和使用,只是告诉你代理不通。可以按两个方向解决:

方向一:留在 NAT 模式,按本节前面三步配。 开 allow-lan → 放行防火墙 → 用网关 IP 而不是 127.0.0.1。配好之后警告可能还在(它只看 Windows 那边的注册表设置),但 WSL 里的代理已经能用了,proxy_status 出口 IP 变了就说明成了。如果不想再看到这行提示,在 .wslconfig[wsl2] 段里把 autoProxy 显式设成 false

方向二:换 mirrored 模式,从根上消除。 镜像模式下两边共用网络栈,127.0.0.1 就是 Windows 的 loopback,这个警告不会再出现,代理也不用做任何额外配置。做法见下一节。

顺便说一个容易搞混的点:这行警告和 autoProxy=true 是两件事。autoProxy=true 只是让 WSL 尝试继承 Windows 的代理设置,但如果继承过来的地址是 127.0.0.1,在 NAT 模式下照样连不通 —— 继承了一个用不了的地址,反而更难排查。所以我在 NAT 模式下建议 autoProxy=false,代理用第三步的显式配置,行为完全可控。

Mirrored 模式下的代理配置

如果你用的是 Windows 11 22H2 及以上,我更推荐直接换 mirrored——代理配置能省掉一大半。

1. 启用 mirrored

编辑 C:\Users\<你的用户名>\.wslconfig

[wsl2]
networkingMode=mirrored
dnsTunneling=true
autoProxy=true
firewall=true

[experimental]
hostAddressLoopback=true

各项作用:

networkingMode=mirrored   与 Windows 共用网络栈,两边 localhost 一致
dnsTunneling=true         改善 VPN / 复杂网络下的 DNS 兼容性
autoProxy=true            继承 Windows 系统代理设置
firewall=true             让 Windows 防火墙策略同样作用于 WSL
hostAddressLoopback=true  允许用主机真实 IP 互访(不只是 127.0.0.1)

然后执行 wsl --shutdown,重开终端后用 ip -brief addr 确认:看到和 Windows 一样的网卡 IP、并且多出 loopback0,就说明换成功了。

2. 配置代理

这是 mirrored 最大的好处:直接用 127.0.0.1

export http_proxy=http://127.0.0.1:7897
export https_proxy=$http_proxy
export all_proxy=socks5h://127.0.0.1:7897
export no_proxy='localhost,127.0.0.1,::1,*.local'
export HTTP_PROXY="$http_proxy"
export HTTPS_PROXY="$https_proxy"
export ALL_PROXY="$all_proxy"
export NO_PROXY="$no_proxy"

对比 NAT 模式,这里省掉了三件事:不需要开 allow-lan(127.0.0.1 就是 Windows 的 loopback)、不需要加防火墙规则(loopback 流量不过防火墙)、不需要动态取网关(地址固定,不会漂移)。

这段同样可以放进 /etc/profile.d/proxy.sh,把 NAT 那版里的动态网关换成 127.0.0.1 就行,proxy_on / proxy_off 两个函数照样留着方便切换。

另外开了 autoProxy=true 之后,很多情况下 WSL 会自动带上代理环境变量,先用 env | grep -i proxy 和上面那条 cdn-cgi/trace 测一下——如果出口 IP 已经是代理的地址,那就什么都不用配。

3. Mirrored 的几个注意点

换之前先知道这些,免得踩坑再回滚:

端口会和 Windows 冲突。WSL 里监听 0.0.0.0:3000 和 Windows 上监听 3000 的程序会互相抢端口。开发常用端口(3000、5173、8080)要留意。

Docker / 桥接网络兼容性。Docker Desktop 的 WSL 后端、需要自定义桥接或 macvlan 的场景,在 mirrored 下偶有问题。如果你重度依赖这些,NAT 更稳。

入站访问受防火墙约束firewall=true 会让 Windows 防火墙策略作用到 WSL,从局域网访问 WSL 里的服务可能需要额外放行。

需要 Windows 11 22H2+。Windows 10 不支持,只能用 NAT 方案。

4. 想回退到 NAT

.wslconfig 改回 networkingMode=NAT 那一套(见本章「NAT 模式的基础配置」),然后 wsl --shutdown。记得同时把代理配置里的 127.0.0.1 换回动态网关写法,否则回退后代理会失效。


五、补齐开发工具

不要把 WSL 当成“又一个黑盒”。网络通了之后,先把基础工具补齐。在 PowerShell 里执行 wsl 进入 Ubuntu,或者直接从开始菜单打开 Ubuntu。

基础工具

sudo apt update
sudo apt upgrade -y
sudo apt install -y git curl wget unzip zip build-essential ripgrep fd-find jq tree

ripgrep 别省,AI Agent 搜代码几乎都优先调 rg,比 grep -R 快一个量级。

Node.js 和 npm

三个 Agent 都是 npm 包,Node 必须装。别直接 apt install nodejs,Ubuntu 仓库的版本偏旧,跑新版 CLI 容易报错。用 NodeSource 官方源装当前 LTS:

curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v && which node

which node 必须是 /usr/bin/node。如果输出 /mnt/c/... 这种 Windows 路径,说明你在 WSL 里调用了 Windows 的 Node,第六章最后一节专门讲这个坑。

注意 sudo 默认会清空环境变量,代理也就跟着丢了,所以上面用的是 sudo -E(保留环境变量)。同理,sudo apt 走不了代理,需要的话单独给 apt 配:

echo "Acquire::http::Proxy \"http://$(ip route show default | awk '{print $3}'):7897\";" | sudo tee /etc/apt/apt.conf.d/95proxy

Python

Ubuntu 24.04 自带 Python 3.12,用 sudo apt install -y python3-pip python3-venv 补上 pip 和虚拟环境模块。

24.04 开始强制 PEP 668,全局 pip install 会被直接拒绝,用虚拟环境或者装 uv(更快,现在更推荐):

curl -fsSL https://astral.sh/uv/install.sh | sh

顺手的工具

sudo apt install -y gh tmux htop

gh 是 GitHub 官方 CLI,Agent 开 PR、看 issue 用得上,装完执行 gh auth login 授权。tmux 能让 Agent 的长任务在你关掉终端后继续跑。

哪些步骤需要代理

必须挂代理的是 deb.nodesource.com(Node)、astral.sh(uv)和 gh auth login。apt 官方源国内可以直连,嫌慢的话换清华镜像。

别让 Windows Node 和 WSL Node 混在一起

这是 WSL + AI Agent 里另一个非常容易出现的问题。如果你在 Windows 装了 Node.jsnpm,WSL 里又装了一份,检查一下:

which node
which npm

输出应该指向 Linux 环境里的 Node,比如 /usr/bin/node/root/.nvm/versions/node/...。如果是 /mnt/c/Program Files/nodejs/node.exe 这种 Windows 路径,说明你现在实际上在 WSL 里调用 Windows 的 Node,很容易制造诡异问题。

所以我的建议是:Linux 工具尽量留在 Linux,Windows 工具留在 Windows,不要随意把两边的 PATH 混在一起。


六、安装并配置 AI Agent

到了这里,整个环境已经发生了一个关键变化:以前是 Windows → PowerShell → AI Agent,现在是 Windows → WSL → Bash → AI Agent

这时候再安装 Codex、Claude Code、OpenCode,很多问题会自然消失。下面的安装配置是不下载 CC-Switch 的情况下自己配置 WSL 中的各个 CLI。

Codex

我们是在 WSL 中运行,就可以直接按照 Linux 环境来安装:

npm install -g @openai/codex

安装完成之后在 WSL 的文件系统里找到 /root/.codex/config.toml/root/.codex/auth.json(用普通用户的话是 /home/<你的用户名>/.codex/,文件不存在就自己创建)。下面以我自建的小站 https://sivan.bond/ 为例,各位根据自己的站点改 base_url 即可,先写 config.toml

model_provider = "OpenAI"
[model_providers.OpenAI]
name = "OpenAI"
base_url = "https://api.sivan.bond/v1"
wire_api = "responses"
requires_openai_auth = true
supports_websockets = true

认证密钥 auth.json

{
  "auth_mode": "apikey",
  "OPENAI_API_KEY": "你的API密钥"
}

Claude Code

在 WSL 中安装 Claude Code:

npm install -g @anthropic-ai/claude-code

以我自建的网关为例进行最小配置,创建 /root/.claude/settings.json(普通用户是 /home/<你的用户名>/.claude/settings.json):

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://api.sivan.bond",
    "ANTHROPIC_AUTH_TOKEN": "你的API密钥",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-opus-5",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-opus-5",
    "ANTHROPIC_SMALL_FAST_MODEL": "claude-opus-5",
    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
    "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
    "DISABLE_INSTALLATION_CHECKS": "1",
    "ENABLE_TOOL_SEARCH": "1"
  },
  "model": "claude-opus-5"
}

这里有几个容易踩的点,我踩过所以专门说明一下。

ANTHROPIC_AUTH_TOKEN,不要用 ANTHROPIC_API_KEY 前者发的是 Authorization: Bearer <key>,后者发的是 x-api-key: <key>。绝大多数第三方网关只认 Bearer,用错了会拿到 401,而错误信息通常只说「认证失败:请检查 API Key 是否正确」,很容易以为是 key 本身的问题。

四个模型变量都要指向你实际有的模型。 Claude Code 除了主模型,还会用 haiku 跑后台小任务(会话标题、文件摘要之类)。如果你的网关只有 opus 一个模型,不做重定向那些后台调用会静默失败。

model 建议写完整模型名,不要用 opus[1m] 这类别名。 别名要靠客户端映射到官方模型 ID,在第三方网关上不一定对得上。

配置完成,进入项目目录启动:

cd /root/AI_Workspace
claude

应该就能进入 Claude Code 了。如果启动时提示地域限制,说明 WSL 的代理没配好。这里的症状容易误判——直连时 claude.ai 会 302 跳到 app-unavailable-in-region,而那个地址在国内直连是连接超时,所以你看到的不是干脆的报错,而是卡一百多秒然后提示连接失败。回到第四章把代理配通即可。

OpenCode

在 WSL 中安装:

npm install -g opencode-ai

配置 OpenCode,创建 /root/.config/opencode/opencode.jsonc(普通用户是 /home/<你的用户名>/.config/opencode/opencode.jsonc)。OpenCode 用第三方模型需要自己写模型定义,确实有点麻烦,以下是我常用的模型和配置,各位可以参考:

opencode.jsonc
{
  "$schema": "https://opencode.ai/config.json",
  "model": "sivan/gpt-5.6-sol",
  "provider": {
    "sivan": {
      "name": "sivan",
      "npm": "@ai-sdk/openai-compatible",
      //"npm": "@ai-sdk/openai",
      "options": {
        "apiKey": "你的API密钥",
		//端点base_url,可以根据你的站点切换
        "baseURL": "https://api.sivan.bond/v1"
      },
      "models": {
        "gpt-5.6-sol": {
          "name": "gpt-5.6-sol",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text", "image"] },
          "limit": { "context": 372000, "output": 131072 },
          "options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        },
        "gpt-5.6-terra": {
          "name": "gpt-5.6-terra",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text", "image"] },
          "limit": { "context": 372000, "output": 131072 },
          "options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        },
        "gpt-5.6-luna": {
          "name": "gpt-5.6-luna",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text", "image"] },
          "limit": { "context": 372000, "output": 131072 },
          "options": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "max": { "reasoningEffort": "max", "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        },
        "gpt-5.5": {
          "name": "gpt-5.5",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text", "image"] },
          "limit": { "context": 256000, "output": 131072 },
          "options": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoningEffort": "low", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoningEffort": "medium", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoningEffort": "high", "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoningEffort": "xhigh", "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        },
        "glm-5.2": {
          "name": "glm-5.2",
          "attachment": true,
          "modalities": { "input": ["text"], "output": ["text"] },
          "limit": { "context": 1000000, "output": 131072 },
          "options": { "thinking": { "type": "enabled" }, "reasoningEffort": "max" },
          "variants": {
            "high": { "thinking": { "type": "enabled" }, "reasoningEffort": "high" },
            "xhigh": { "thinking": { "type": "enabled" }, "reasoningEffort": "xhigh" },
            "max": { "thinking": { "type": "enabled" }, "reasoningEffort": "max" }
          }
        },
        "grok-4.6": {
          "name": "grok-4.6",
          "attachment": true,
          "modalities": { "input": ["text", "image"], "output": ["text"] },
          "limit": { "context": 500000, "output": 131072 },
          "options": { "reasoningEffort": "high" },
          "variants": {
            "low": { "reasoningEffort": "low" },
            "medium": { "reasoningEffort": "medium" },
            "high": { "reasoningEffort": "high" }
          }
        },
        "deepseek-v4-flash": {
          "id": "deepseek-v4-flash",
          "limit": { "context": 1000000, "output": 65536 },
          "reasoning": true,
          "options": { "reasoningEffort": "xhigh" },
          "variants": {
            "low": { "reasoningEffort": "low" },
            "medium": { "reasoningEffort": "medium" },
            "high": { "reasoningEffort": "high" },
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        },
        "deepseek-v4-pro": {
          "id": "deepseek-v4-pro",
          "limit": { "context": 1000000, "output": 65536 },
          "reasoning": true,
          "options": { "reasoningEffort": "xhigh" },
          "variants": {
            "low": { "reasoningEffort": "low" },
            "medium": { "reasoningEffort": "medium" },
            "high": { "reasoningEffort": "high" },
            "xhigh": { "reasoningEffort": "xhigh" },
            "max": { "reasoningEffort": "max" }
          }
        },
        "claude-opus-5": {
          "name": "claude-opus-5",
          "attachment": true,
          "reasoning": true,
          "modalities": { "input": ["text", "image"], "output": ["text"] },
          "limit": { "context": 1000000, "output": 128000 },
          "options": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoning": { "effort": "low" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoning": { "effort": "medium" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoning": { "effort": "high" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "max": { "reasoning": { "effort": "max" }, "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        },
        "claude-opus-4-8": {
          "name": "claude-opus-4-8",
          "attachment": true,
          "reasoning": true,
          "modalities": { "input": ["text", "image"], "output": ["text"] },
          "limit": { "context": 1000000, "output": 128000 },
          "options": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
          "variants": {
            "low": { "reasoning": { "effort": "low" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "medium": { "reasoning": { "effort": "medium" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "high": { "reasoning": { "effort": "high" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "xhigh": { "reasoning": { "effort": "xhigh" }, "textVerbosity": "medium", "reasoningSummary": "auto" },
            "max": { "reasoning": { "effort": "max" }, "textVerbosity": "medium", "reasoningSummary": "auto" }
          }
        }
      }
    }
  }
}

然后进入项目目录打开 opencode:

cd /root/AI_Workspace
opencode

七、总结

整套环境长什么样

Windows          浏览器、微信、VS Code、文件管理器
  └── WSL 2      bash、git、node、python、AI Agent
        └── /root/AI_Workspace    所有项目

Windows 负责桌面,WSL 负责开发,Agent 跑在 WSL 里面。文章开头那些「引号不匹配」「grep 不存在」「export 无法使用」的报错,基本都是因为 Agent 以为自己在 Linux 里,而你给的是 PowerShell。把它真的放进 Linux,这类问题就不用再管了。

装机顺序

1. 装 WSL(用 --location 放到非系统盘),重启,确认 VERSION = 2
2. 配网络:NAT 三步 或 换 mirrored
3. 装工具:apt 基础包 → Node → Python
4. 建 /root/AI_Workspace,项目放 WSL 自己的文件系统
5. 装 Agent:codex / claude / opencode

顺序别调。第 2 步不做,第 3 步的 Node、uv、gh auth login 全都会卡;第 4 步放错位置,后面所有涉及大量小文件的操作都会慢。

我踩过的几个坑

代理三件套缺一不可。 allow-lan、防火墙规则、环境变量。防火墙那步最容易漏,因为大部分教程都不提,症状是「配置看起来全对但就是连不上」。

NAT 模式的网关 IP 会漂移。 别写死,从 ip route show default 动态取。

sudo 会清掉代理。sudo -Eapt 单独配 95proxy

别让两边的 Node 混在一起。 which node 出现 /mnt/c/... 就是有问题。

发行版别留在 C 盘。 wsl --manage <发行版名> --move D:\... 一条命令就能搬走,ext4.vhdx 长到几十个 G 是常事。

最后

这套环境搭一次,后面新开项目基本零成本。对我来说最大的变化不是快了多少,而是不用再盯着 Agent 反复纠正 shell 语法——它现在的每一条命令都能正常跑,token 和时间都省下来了。

26 个赞

你好呀,可汗佬

你好xiaoyi

:rofl:,agent:老板别生气

:sob:没这么有情绪价值

这头像怎么看着好熟悉,几天没见,已经成大佬了 :xhj16:

必须点赞

1 个赞

希望能够帮到你:face_savoring_food::face_savoring_food:

wsl这么麻烦,干脆直接装双系统得了

双系统也不简单,干脆买mac得了

wsl好像默认必须访问系统盘,能隔离就好了

WSL 默认好像是会把所有 Windows 盘挂到 /mnt/ 下面,也就是 Linux 侧随时能读写整个 C 盘。跑 AI Agent 的时候Agent 来一句 rm -rf /mnt/c/… 就完了。但是,直接把wsl改成不能读windows的盘有时候也不太方便。

这么好的帖子,不是别人ref 我都没看到。

好教程 :xhj003:

路过学习一下

3 个赞

这个技术贴很棒
已经比较全面了,就差一点点,把贴图工具如何配套使用就完整了

让ai看你的教程部署可以吗:pleading_face:

前几天刚刚从win切到wsl中,再也没有powershell错误了

哈哈哈,James!!也是想把自己踩过的坑和整理的经验分享出来,能被大家看到并用上就很好。