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

在react应用中使用模块化css

什么是模块化CSS?

模块化管理CSS,避免全局污染,实现模块化、可服用。

在react中应用

启用CSS Modules

css-loader中增加一个选项:

{
    loader: require.resolve('css-loader'),
    options: {
        importLoaders: 1,
        minimize: true,
        sourceMap: shouldUseSourceMap,
        modules: true, // 启用CSS Modules
    },
},

CSS文件

app.css:

.title{
    color: red;
}

最后会编译成:

._2L1SLeGPg5sisdRmqO9mCH {
  color: red;
}

JavaScript文件

app.js:

import styles from './app.css';

class App extends Component {
  render() {
    return <div className={styles.title}>Hello World.</div>;
  }
}

最后会编译成:

<div class="_2L1SLeGPg5sisdRmqO9mCH">Hello World.</div>