我为什么开始从 Claude Code 转向 Codex:一次真实的 AI Coding 工具迁移记录

过去一段时间,我一直在用 Claude Code 做日常开发。

它有两个很明显的优点:一是上手快,二是终端里的使用方式很符合程序员习惯。提需求、看改动、继续追问,整个流程比传统聊天框更接近真实开发。

所以很长一段时间里,我对 Claude Code 的评价都不错。
它不是那种只能“看看热闹”的 AI 工具,而是真的能用起来。

但最近,我开始越来越多地把主力切到 Codex。

原因不是谁突然“碾压”了谁,也不是单看参数得出的结论。更真实的原因是:当 AI 开始真正进入开发流程之后,我在意的东西变了。

我不再只看它会不会生成一段代码,而是更在意这些问题:

  • 它能不能看懂一个真实项目
  • 它改代码时稳不稳
  • 它能不能处理多文件、多模块的修改
  • 它是不是适合长期作为主力工具

也是在这些维度上,我开始明显感觉到:Codex 更像一个面向工程流的工具。

AI Coding 的关键,不只是“会写代码”

刚开始用 AI Coding 工具时,大家最容易关注的是:

  • 会不会写 React
  • 会不会写 Node.js
  • 一次能生成多少代码
  • 回答看起来聪不聪明

但用久了之后会发现,这些都不是最关键的。

真正重要的是:
它到底能不能帮你把真实项目往前推进。

因为真实开发不是从零写一个 demo,而是面对:

  • 已经存在的仓库
  • 不统一的历史代码
  • 多个文件之间的依赖关系
  • 改一处可能影响多处的复杂逻辑

这时候,你需要的不是“临时回答一个问题”的 AI,
而是一个能进入项目语境、理解上下文、尽量稳定修改代码的工具。

这也是我越来越看重 Codex 的原因。

为什么我开始更偏向 Codex

我现在最明显的感受,是 Codex 更像是在“读仓库”,而不是“回答问题”。

这两者差别很大。

“回答问题”式的工具,适合快速生成、快速试验。
“读仓库”式的工具,更适合真实开发。

比如一个实际需求,往往不是只改一个文件,而是会牵动:

  • 页面组件
  • 状态管理
  • API 层
  • 类型定义
  • 构建或配置逻辑

这时候,一个工具能不能在多文件上下文里稳定工作,就很关键。

我的实际体验是,Codex 在这类任务里更容易形成连续的工作过程。
不是回你一段代码就结束了,而是更像在参与一段真正的开发流程。

另外一点是,它在“改已有代码”这件事上,更符合我的预期。

因为生成新代码不难,难的是在现有系统上做修改。
有些 AI 工具第一次看很惊艳,但真放进项目里,会出现这些问题:

  • 改动表面合理,实际上破坏原逻辑
  • 修一个问题,又引入新的问题
  • 过度重写,增加 review 成本

相比之下,我现在会更信任 Codex 一些。
不是说它不会出错,而是它更容易给出“像在认真修改现有项目”的结果。

那 Claude Code 还有没有价值?

当然有。

而且我不觉得这件事应该写成“谁赢谁输”。

在我看来,它们只是更适合不同场景。

Claude Code 依然适合:

  • 快速试验一个想法
  • 处理轻量项目
  • 更灵活地尝试不同模型或组合方案
  • 作为备选工具做交叉验证

所以我现在不是完全不用 Claude Code 了。
更准确地说,是它从“主力”慢慢变成了“仍然值得保留的工具”。

我的结论

我现在的判断很简单:

Claude Code 依然是个好工具。
对于个人开发者、轻量项目、快速试验,它仍然有价值。

但如果你已经开始认真把 AI 用进真实开发里,尤其是越来越在意:

  • 仓库理解能力
  • 多文件修改能力
  • 改代码的稳定性
  • 持续协作体验

那 Codex 更值得你认真投入。

未来 AI Coding 工具真正竞争的,未必只是模型能力。
更重要的,是谁能更稳定地融入开发者每天真实的工程流程。

谁能进入工作流,谁才更可能成为主力。

而对我来说,Codex 现在越来越像那个主力。

Client Tool 是什么?为什么它正在成为 AI 应用架构的关键能力

随着大模型能力的增强,工具调用(Tool Calling)已经成为构建智能 Agent 的核心机制。过去我们更多讨论的是“服务端工具(Server-side Tools)”,例如数据库查询、搜索 API、代码执行环境等。

但最近,一个新的模式开始越来越重要 —— Client Tool(客户端工具)

这篇文章我们系统讲清楚:

  • Client Tool 是什么?
  • 它和传统 Server Tool 有什么区别?
  • 为什么它在 Web / 移动端 Agent 场景中至关重要?
  • 实际架构如何设计?
  • 典型应用场景有哪些?

一、什么是 Client Tool?

Client Tool 是运行在客户端(浏览器 / App / 本地环境)中的工具能力,并由模型通过 tool calling 机制触发执行。

简单理解:

Server Tool 在服务器执行
Client Tool 在用户设备执行

模型并不直接操作客户端,而是:

  1. 模型输出 tool call 指令
  2. 前端解析该 tool call
  3. 在本地执行对应逻辑
  4. 将结果回传给模型

二、Server Tool vs Client Tool 对比

维度 Server Tool Client Tool
运行位置 后端服务器 浏览器 / App / 本地
数据访问 数据库、内部服务 DOM、LocalStorage、摄像头、文件系统
网络依赖 必须依赖 可以离线执行
安全性 服务器控制 需沙箱 & 权限控制
典型场景 查数据库、调用API 操作UI、读本地文件、调用设备能力

三、为什么 Client Tool 很重要?

1️⃣ AI 正在进入“应用层”,而不是“问答层”

传统 Chat 只是文本对话。

但真正的 AI 应用需要:

  • 操作界面
  • 控制组件
  • 调用本地能力
  • 与用户设备交互

这些能力必须发生在客户端。


2️⃣ Web Agent 必须操控前端状态

例如:

  • 填写表单
  • 切换页面
  • 打开弹窗
  • 修改 React 状态
  • 读取当前页面 DOM

这些操作服务器根本无法完成。


3️⃣ 数据隐私 & 本地数据

Client Tool 可以访问:

  • 本地文件
  • 浏览器缓存
  • IndexedDB
  • 本地录音
  • 摄像头数据

这些数据你不一定希望上传到服务器。


四、典型架构设计

一个标准的 Client Tool 架构通常如下:

User
  ↓
Frontend App
  ↓
LLM (with tool calling)
  ↓
Tool Call JSON
  ↓
Frontend Tool Dispatcher
  ↓
Local Tool Execution
  ↓
Result → 回传 LLM

关键模块

1️⃣ Tool Registry(前端)

const tools = {
  openModal: ({ name }) => { ... },
  readLocalFile: async () => { ... },
  getPageState: () => { ... }
}

2️⃣ Tool Dispatcher

if (message.tool_call) {
   const tool = tools[message.tool_call.name]
   const result = await tool(message.tool_call.arguments)
}

3️⃣ 权限控制(必须)

  • 白名单机制
  • 用户确认
  • 沙箱执行
  • 参数校验

否则等于把用户浏览器完全交给模型。


五、Client Tool 的典型场景

🟢 1. 操作 UI

  • 自动填写表单
  • 自动生成表单配置
  • 自动布局页面
  • 打开指定组件

典型场景:低代码平台 + AI


🟢 2. 本地文件处理

  • 读取用户上传的 PDF
  • 本地解析 CSV
  • 本地生成文档

适合做:
知识管理系统 / 本地 Agent


🟢 3. 设备能力调用

  • 打开摄像头
  • 开始录音
  • 调用蓝牙
  • 调用 NFC

移动端 AI 应用非常关键。


🟢 4. 浏览器环境感知

  • 当前 URL
  • 页面结构
  • 选中的文本
  • 用户滚动位置

这类能力对“网页助手型 Agent”非常重要。


六、Client Tool 的安全问题

这是很多人忽略的部分。

Client Tool 实际上是:

给模型执行本地代码的能力

如果设计不好,会出现:

  • 任意 DOM 操作
  • 读取敏感信息
  • 自动提交表单
  • 发起请求

必须做的三件事:

  1. 参数校验(Schema validation)
  2. 明确工具白名单
  3. 敏感工具必须二次确认

七、Client Tool + Server Tool 混合架构

真正成熟的系统一定是混合模式:

  • Server Tool 处理重计算 & 数据库
  • Client Tool 处理交互 & 本地能力

例如:

用户说:

帮我分析这个本地 PDF,并生成一个总结页面

流程:

  1. Client Tool 读取本地 PDF
  2. 上传文本到服务器
  3. Server Tool 做 embedding + 分析
  4. Client Tool 渲染页面

这才是完整闭环。


八、Client Tool 是 Agent 架构进化的关键一步

过去我们关注的是:

Prompt Engineering

现在我们正在进入:

Tool Orchestration Engineering

而 Client Tool 让 Agent 不再只是“对话系统”,而是“应用控制系统”。


九、未来趋势

  • 浏览器内嵌 Agent Runtime
  • React / Vue 组件级 Tool 暴露
  • AI 驱动 UI 编排
  • 本地推理 + 本地 Tool 执行
  • 移动端 Agent OS 级能力整合

总结

Client Tool 的本质:

把模型能力延伸到用户设备执行层

它解决的是:

  • AI 与应用之间的最后一公里
  • AI 与真实世界交互的问题

如果你在做:

  • AI Agent
  • 低代码平台
  • 浏览器插件
  • AI 助手型 App
  • SaaS 智能增强

那么 Client Tool 基本是必选项。

使用 GitHub Actions 定时自动备份数据库(MySQL / MongoDB)

使用 GitHub Actions 定时自动备份数据库(MySQL / MongoDB)

在日常开发或部署中,数据库备份是保证数据安全的重要环节。但手动备份不仅麻烦,也容易忘记。好在我们可以利用 GitHub Actions 的定时任务(schedule)能力 每天自动备份数据库,并上传到 GitHub、OSS 或服务器。

本文将介绍如何:

  1. 使用 GitHub Actions 定时执行备份
  2. 通过 SSH 登录服务器执行备份脚本
  3. 或直接在 GitHub Actions 中连接远程数据库备份
  4. 将备份文件上传到 GitHub Releases / 阿里云 OSS / AWS S3

一、准备工作

1. 在 GitHub 仓库中配置 Secrets

进入:

Settings → Secrets and variables → Actions → New repository secret

需要设置:

Secret 名称 说明
SSH_PRIVATE_KEY 用于连接服务器(如果你在服务器执行备份)
DB_HOST 数据库地址
DB_USER 数据库用户名
DB_PASSWORD 数据库密码
DB_NAME 需要备份的库名

如果你在 GitHub Actions 内部直连数据库,则只需 DB 相关的 secret。


二、方法一:通过 SSH 登录服务器备份(最佳方案)

这种方式最稳定:
✔ 不暴露数据库端口
✔ 备份在服务器本地执行
✔ 支持 Docker 环境、物理机、云主机

1)服务器备份脚本 example

创建 /root/backup/mysql_backup.sh

#!/bin/bash
set -e

DATE=$(date +"%Y%m%d_%H%M%S")
BACKUP_DIR="/root/backups/mysql"
mkdir -p $BACKUP_DIR

# 备份文件名
FILE="$BACKUP_DIR/${DATE}.sql.gz"

echo "开始备份: $FILE"

mysqldump -u root -p'密码改为你的' --databases gfds | gzip > "$FILE"

echo "备份完成:$FILE"

2)GitHub Actions workflow

.github/workflows/db-backup.yml

name: Database Backup

on:
  schedule:
    - cron: "0 18 * * *" # 每天 02:00(中国时间 UTC+8)
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.8.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H your-server-ip >> ~/.ssh/known_hosts

      - name: Execute backup script on server
        run: |
          ssh root@your-server-ip "bash /root/backup/mysql_backup.sh"

只要配置好 SSH key,这个 workflow 就会每天自动执行
备份会保存在服务器 /root/backups/mysql/


三、方法二:在 GitHub Actions 中直接备份远程数据库

适用于支持外网访问数据库的场景。

MySQL 备份示例

name: Backup MySQL

on:
  schedule:
    - cron: "0 18 * * *"
  workflow_dispatch:

jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - name: Install MySQL client
        run: sudo apt-get update && sudo apt-get install -y mysql-client

      - name: Backup database
        run: |
          FILE="backup_$(date +%Y%m%d_%H%M%S).sql.gz"
          mysqldump -h ${{ secrets.DB_HOST }} \
            -u ${{ secrets.DB_USER }} \
            -p${{ secrets.DB_PASSWORD }} \
            ${{ secrets.DB_NAME }} | gzip > $FILE
          echo "备份文件:$FILE"

      - name: Upload backup as artifact
        uses: actions/upload-artifact@v4
        with:
          name: mysql-backup
          path: "*.sql.gz"

四、方法三:备份 MongoDB

在服务器执行备份

mongo_backup.sh

DATE=$(date +"%Y%m%d_%H%M%S")
DIR="/root/backups/mongo"
mkdir -p $DIR

mongodump --gzip --archive="$DIR/$DATE.gz"

echo "MongoDB 备份完成:$DIR/$DATE.gz"

GitHub Actions 同上,只是换一下脚本名。


五、上传备份到 GitHub / OSS / S3

你可以在 backup job 最后追加:

上传到 GitHub Releases

      - name: Upload to GitHub Releases
        uses: softprops/action-gh-release@v2
        with:
          tag_name: "backup-${{ github.run_id }}"
          files: "*.gz"

上传到阿里云 OSS

      - name: Upload to Aliyun OSS
        uses: manyuanrong/aliyun-oss-website-action@v1.1.9
        with:
          accessKeyId: ${{ secrets.OSS_ID }}
          accessKeySecret: ${{ secrets.OSS_SECRET }}
          bucket: backup-bucket
          endpoint: oss-cn-hangzhou.aliyuncs.com
          folder: db-backups
          localFolder: .

六、总结

GitHub Actions + 定时任务(cron)可以轻松实现自动化数据库备份
根据你的系统结构,你可以选择:

场景 推荐方案
服务器上有数据库 SSH 登录服务器执行备份(最安全)
数据库允许外网访问 在 GitHub Actions 中直接备份
需要长期保存备份 上传 GitHub Releases / OSS / S3

前端使用浏览器实现asr语音识别


// 创建一个新的SpeechRecognition对象 var recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition)(); recognition.lang = 'en-US'; // 设置语言 recognition.interimResults = false; // 设置是否返回临时结果 recognition.maxAlternatives = 1; // 设置返回的最大替代词数量 // 当识别到语音结束时,返回结果 recognition.onresult = function(event) { console.log('You said: ', event.results[0][0].transcript); }; // 开始语音识别 recognition.start();

demo

 

react-native仿微信通讯录右侧边栏快速定位功能

代码地址 -> ReactNativeCountrySelect

1. 界面

SectionList把数据渲染出来

右边的A-ZText组件即可,这里为了做滑动定位,没有选择Touchable组件

import * as React from 'react';
import {
  Text,
  View,
  StyleSheet,
  SectionList,
  SafeAreaView,
} from 'react-native';

import countries from './countryCode.json';
const sectionMapArr = [
  ['A', 0],
  ['B', 1],
  ['C', 2],
  ['D', 3],
  ['E', 4],
  ['F', 5],
  ['G', 6],
  ['H', 7],
  ['I', 8],
  ['J', 9],
  ['K', 10],
  ['L', 11],
  ['M', 12],
  ['N', 13],
  ['O', 14],
  ['P', 15],
  ['Q', 16],
  ['R', 17],
  ['S', 18],
  ['T', 19],
  ['U', 20],
  ['V', 21],
  ['W', 22],
  ['X', 23],
  ['Y', 24],
  ['Z', 25],
];
export default class App extends React.Component {
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <SectionList
          containerStyle={{ flex: 1, justifyContent: 'center' }}
          ItemSeparatorComponent={() => (<View style={{ borderBottomColor: '#F8F8F8', borderBottomWidth: 1, }} />)}
          renderItem={({ item, index, section }) => (
            <View style={styles.itemContainer}>
              <Text style={styles.itemText} key={index}>
                {item.countryName}
              </Text>
            </View>
          )}
          renderSectionHeader={({ section: { key } }) => (
            <View style={styles.headerContainer}>
              <Text style={styles.headerText}>{key}</Text>
            </View>
          )}
          sections={countries}
          keyExtractor={(item, index) => item + index}
        />
        <View
          style={{ width: 16, justifyContent: 'center' }}
        >
          {sectionMapArr.map((item, index) => {
            return (
              <Text
                key={index}
              >
                {item[0]}
              </Text>
            );
          })}
        </View>
      </SafeAreaView>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#FFFFFF',
    flexDirection: 'row',
  },
  headerContainer: {
    padding: 5,
    backgroundColor: '#F8F8F8',
  },
  headerText: {
    fontWeight: 'bold',
  },
  itemContainer: {
    paddingHorizontal: 5,
    paddingVertical: 10,
  },
  itemText: {
    fontSize: 16,
  },
});

这时候界面已经完成了,然后就是增加触摸滑动定位的功能了。

2. 使用Gesture Responder System监听触摸事件

给右侧的View上启用手势


<View style={{ width: 16, justifyContent: 'center' }} onStartShouldSetResponder={() => true} onMoveShouldSetResponder={() => true} > {sectionMapArr.map((item, index) => { return ( <Text key={index} > {item[0]} </Text> ); })} </View>

这样我们就可以在触摸滑动的时候,获得滑动到的位置

3. 使用onLayout找到每个字母对应的X、Y

首先在constructor里声明一个实例属性,用来记录每个字母的信息:

this.ps = [];

然后在Text组件上利用onLayout获得每个字母的位置,并且存到this.ps里:

<Text
  key={index}
  onLayout={({
    nativeEvent: {
      layout: { x, y, width, height },
    },
  }) => {
    this.ps = this.ps.filter(i => i.key !== item[0]); 
    this.ps.push({
      key: item[0],     // 对应的字母 A-Z
      min: y,           // 字母顶部Y坐标
      max: y + height,  // 字母底部Y坐标
      index: item[1],   // 字母对应SectionList的index
    });
  }}
>
  {item[0]}
</Text>

4. 根据滑动找到滑到哪个字母上

<View
  style={{ width: 16, justifyContent: 'center' }}
  onStartShouldSetResponder={() => true}
  onMoveShouldSetResponder={() => true}
  onResponderMove={({ nativeEvent: { pageY } }) => {
    const offsetY = pageY - this.offsetY;
    const find = this.ps.find(
      i => i.min < offsetY && i.max > offsetY
    );
    if (find) {
      console.log(find) // 滑动到的字母
    }
  }}
>

5. 根据触摸的字母,SectionList跳到对应的位置

  1. 先在constructor里创建ref
this.sectionlist = React.createRef();
  1. 在SectionList上绑定ref
ref={this.sectionlist}
  1. 调用SectionList的scrollToLocation
onResponderMove={({ nativeEvent: { pageY } }) => {
  const offsetY = pageY - this.offsetY;
  const find = this.ps.find(
    i => i.min < offsetY && i.max > offsetY
  );
  if (find) {
    this.sectionlist.current.scrollToLocation({
      sectionIndex: find.index,
      itemIndex: 0,
      animated: false,
    });
  }
}}

完工

react native改变WebView背景颜色

react-native的WebView背景颜色是无法改变的,不过我们可以用另一个View盖住WebView,达到改变颜色的效果。

import React from 'react';
import {View, StyleSheet, WebView, ActivityIndicator} from 'react-native';

export default class YourComponent extends React.PureComponent{

    state = {
        loading: true,
    };

    render() {
        return (
            <View>
                <WebView
                    source={html}
                    onLoadEnd={() => {
                        this.setState({loading: false});
                    }}
                />
                {
                    this.state.loading && (
                        <View style={{
                            ...StyleSheet.absoluteFillObject,
                            backgroundColor: '#000000',  // your color 
                            alignItems: 'center',
                            justifyContent: 'center',
                        }}>
                            <ActivityIndicator />
                        </View>
                    )
                }
            </View>
        )

    }
}

Chrome中from memory cache和from disk cache

Chrome中文件缓存有Memory Cache和Disk Cache两种

顾名思义

  • Memory Cache:放在内存中的缓存

  • Disk Cache:放在硬盘上的缓存

一些规律:

  1. 第一次打开页面的时候,是没有缓存的,直接请求资源

  2. 刷新页面(⌘+R)的时候,会发现有些文件是from memory cache,有些是from disk cache

  3. 关掉浏览器,再打开页面,没有memory cache了

  4. 无痕窗口下,资源都会放在memory cache,关掉窗口缓存就没了,不会留下痕迹

  5. 图片会优先放进memory cache

  6. 小文件会优先放进memory cache

  7. 大文件几乎是disk cache

最后送上一张http缓存的图片

iOS开发:在Swift中使用Alamofire发送HTTP请求

创建项目

  1. 打开Xcode,点击Create a new Xcode project

  2. 选择Single View App,点击Next

  3. 输入Product Name:demo,Language选择Swift,点击Next

  4. 选择一个目录存放你的项目

使用Pod安装Alamofire

  1. 关掉Xcode

  2. 在你的项目目录里,创建一个文件Podfile

  3. 输入

    source 'https://github.com/CocoaPods/Specs.git'
    platform :ios, '10.0'
    use_frameworks!
    
    target 'demo' do
        pod 'Alamofire', '~> 4.7'
    end
    
  4. 在终端中输入:pod install

  5. 打开demo目录,双击demo.xcworkspace打开项目

使用Alamofire发送HTTP请求

  1. 打开文件ViewController.swift

  2. 在viewDidLoad函数内调用Alamofire

    Alamofire.request("https://api.github.com/gists").responseJSON { response in
        print("Request: \(String(describing: response.request))")   // original url request
        print("Response: \(String(describing: response.response))") // http url response
        print("Result: \(response.result)")                         // response serialization result
    
        if let json = response.result.value {
            print("JSON: \(json)") // serialized json response
        }
    
        if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
            print("Data: \(utf8Text)") // original server data as UTF8 string
        }
    }
    
  3. ⌘+R启动APP,可以看到输出

使用Gatsby生成静态网站并部署在GitHub上

什么是Gatsby

Blazing-fast static site generator for React

Gatsby是一个基于React极其快的静态网站生成工具

支持各种数据源,markdown、Wordpress等

创建网站

  1. 安装Gatsby命令行工具:
    npm install --global gatsby-cli
    
  2. 创建一个新的网站
    gatsby new sheng00.cn
    
  3. 运行刚才创建的网站
    cd sheng00.cn
    gatsby develop
    

    打开localhost:8000即可看到

部署在GitHub上

  1. 在GitHub上创建一个repository

  2. 在刚才生成的网站运行下面的命令

    git add -A
    git commit -m "first commit"
    git remote add origin git@github.com:shengoo/sheng00.cn.git
    git push -u origin master
    
  3. 安装gh-pages
    yarn add gh-pages
    
  4. 在package.json里增加一个脚本
    "deploy": "gatsby build --prefix-paths && gh-pages -d public"
    
  5. 部署到GitHub
    yarn deploy
    

设置自定义域名

  1. 从域名注册商那里,把域名指向yourusername.github.io

  2. 在GitHub的repository的设置里,设置Custom Domain:www.sheng00.cn

  3. 在Gatsby生成的网站中,新建一个目录static,创建一个文件CNAME

    www.sheng00.cn
    
  4. 再次部署
    yarn deploy
    

源代码网址:https://github.com/shengoo/sheng00.cn