移动开发技术

Jason's Blog

用AI编写业务代码 - MCP的应用

| Comments

如何用AI写业务代码

我们知道,用AI写通用代码是非常容易且准确的,例如很容易让AI写一个冒泡排序或者时间格式处理工具函数,因为AI大模型训练的素材就来源于互联网上的通用知识和算法。但是,AI并没有通过我们的业务代码进行训练,所以按道理来讲,AI是写不出我们的业务代码的。那我们如何让AI变得也会写我们的业务代码呢,本文就是想解决这个问题。

如何让AI理解业务

要让AI写出业务代码,那就等价于需要让AI理解业务,就目前主流的方式,有以下四种:

  1. function calling 函数调用

  2. MCP 模型上下文协议(今天的主题)

  3. RAG 检索增强生成

  4. Fine-tuning 微调

什么是MCP

MCP全称是Model Context Protocol,翻译为模型上下文协议。MCP是一种以json格式的明文传输协议。

以一个城市温度查询服务的MCP为例,它的请求和返回格式例如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
    "jsonrpc": "2.0",
    "id": "req_123", // 请求唯一标识
    "method": "execute_tool",
    "params": {
        "tool": "weather_query",
        "arguments": {
            "location": "北京",
            "unit": "celsius"
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "jsonrpc": "2.0",
    "id": "req_123", // 匹配请求ID
    "result": {
        "status": "success",
        "data": {
            "location": "北京",
            "temperature": 28,
            "unit": "celsius",
            "condition": "晴朗"
        }
    }
}

MCP服务有两种启动形式,第一种是stdio,以本地rpc的方式和大模型进行通信;第二种是http server,与大模型可以本地或者远程通信。

MCP本质上是规定了一个统一标准协议,让大模型可以方便的调用外部工具。说到这里,让笔者想起来以前一个很火的概念SOA(Service-Oriented Architecture),这么多年兜兜转转还是这些思路。

大模型与MCP的结合应用

有了MCP能力,我们就可以将业务里的工具以MCP的方式进行封装,供大模型使用。例如我们可以让大模型去调用我们的生成代码服务,并将生成的代码返回给大模型。这时候大模型拿到的代码就不是一个通用知识代码,而是一个符合我们业务领域知识的代码。由于MCP工具是确定性的,所以通过这种方式,我们可以减少大模型输出的幻觉问题,让大模型可以稳定高质量输出我们想要的业务代码。

例如,我们可以有以下一些MCP服务:

  1. pb2ts:根据开发者输入的Protobuf协议文件,生成前端Typescript网络请求代码,并将相关代码自动上传到项目git仓库,更新本地仓库文件。开发者可以直接在IDE里使用最新的Typescript代码完成网络请求功能。

  2. 拉起游戏:比如我们的项目需要拉起一个外部游戏或者App,那么我们可以让MCP返回固定的业务代码,通过某个SDK来进行外部拉起

    • RAG:我们的MCP还可以连接RAG向量检索,进行相关的查询服务,将数据结果返回给大模型分析和使用

在这种情况下,AI大模型用作自然语言的语义理解和意图识别,然后判断调用哪些MCP服务来完成功能,这正是大模型的强项。而MCP的任务是按照标准协议提供项目特定的工具给大模型,使大模型的输出更加有确定性,减少大模型幻觉产生,并且能够理解我们的业务相关功能和代码。

pb2code MCP的功能和实现

笔者在项目里实现了一个pb2code服务,用户可以输入pb协议文件地址,生成业务特定的网络请求代码。具体的交互如下:

开发者只需要在AI聊天框里说:根据某个协议生成某个网络请求代码,就可以很方便完成相关代码的编写。MCP服务完成了以下事情:

  1. 拿到大模型解析出来的pb文件网络地址和请求名两个参数

  2. 调用项目内的研发平台服务,(1)生成ts代码(2)代码上传git(3)生成请求mock数据

  3. 输出命令 git submodule update --remote 让大模型更新子仓库

  4. 输出业务代码,让大模型将业务代码写入代码编辑区

这段网络请求代码是基于我们项目的网络框架编写的,如果让大模型基于通用知识去写,是无法写出这样的特定业务逻辑代码的。

各模块整体交互流程如下图:

思考与展望

基于MCP,我们可以提供更多的模块服务,辅助AI来编写业务代码。除了调用外部工具之外,我们还可以借助RAG技术,进行业务代码的检索,然后通过MCP的方式提供接口给大模型调用,后面可能会在这方面做一些尝试。

Npm和pnpm对第三方库的安装和查找区别

| Comments

npm 和 pnpm 安装的区别

npm是将依赖安装到项目根目录node_modules里。

pnpm虽然也将依赖安装到node_modules里,但真实的包是在.pnpm里的,每个node_modules里都指向.pnpm里的唯一数据。

举个例子,package.json的内容如下

1
2
3
4
5
6
{
    "dependencies": {
        "A": "^1.0.0",
        "B": "^1.0.0"
    }
}

其中A依赖了C@2.0.0,B依赖了C@3.0.0,两种安装方式的目录结构如下:

  • npm安装
1
2
3
4
5
6
7
node_modules
    A@1.0.0
        node_modules
            C@2.0.0
    B@1.0.0
        node_modules
            C@3.0.0

C如果安装在node_modules里,会造成版本冲突,所有将不同版本的C安装到了子node_modules里。当然,如果C的版本相同,会提取到根目录的node_modules里,这样不会造成冲突,也节省空间,但会丢失依赖树结构。

  • pnpm安装

    最后用pnpm安装的目录为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
node_modules
    .pnpm
        A@1.0.0
            node_modules
                A
                C -> .pnpm/C@2.0.0/node_modules/C
        B@1.0.0
            node_modules
                B
                C --> .pnpm/C@3.0.0/node_modules/C
        C@2.0.0
            node_modules
                C
        C@3.0.0
            node_modules
                C
    A -> .pnpm/A@1.0.0/node_modules/A
    B -> .pnpm/B@1.0.0/node_modules/B

可以看到:

  • pnpm所有安装的库都在.pnpm目录下,每个版本号一个单独的目录,其余的地方都是引用到这里的

  • 根node_modules下只有项目直接使用的库A和B,指向.pnpm对应位置

  • A和B的子依赖放在子node_modules目录里,并指向唯一的存储.pnpm目录

pnpm解决了版本冲突的问题,节省了硬盘空间,并保留了依赖树结构。

pnpm安装后库的查找过程

  1. 代码引用A@1.0.0,在node_modules/A查找,实际位置是node_modules/.pnpm/A@1.0.0/node_modules/A

  2. A依赖C,向上一级目录的node_modules里能查找到C,即node_modules/.pnpm/A@1.0.0/node_modules/C,实际指向位置是.pnpm/C@2.0.0/node_modules/C

vite项目打包遇到的问题和解决方案

在项目使用pnpm后,如果A引入了B,当pnpm install A时,如果项目不直接安装B,会在编译时出现报错,查找不到B模块

经过查找资料,发现vite打包用的rollup,只能打包相对模块,对于其他模块是不会被打包到bundle里的。

解决方案在vite.config.ts文件中增加配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import { nodeResolve } from '@rollup/plugin-node-resolve';

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    uni(),
  ],
  build: {
    rollupOptions: {
      plugins: [nodeResolve()],
    }
  }
});

参考资料 Troubleshooting | Rollup

除了以上方法,还发现另外一个解决方案,将第三方库pnpm install的时候进行平铺安装,需要在.npmrc文件里增加配置

1
shamefully-hoist = true

这种方式安装,会将所有的库都安装在node_modules下,这时候是可以成功查找二级依赖并打包成功的。

Nginx日志切割

| Comments

随着nginx日志越来越大,最终会将服务器上的日志占满,方案是用logformat自动进行日志切割,需要以下步骤

Hippy接入typescript记录

| Comments

项目的package.json如下

1
2
3
4
5
6
7
8
9
10
11
12
  "scripts": {
    "dev": "vue-cli-service serve --https true --port 443 --host test.igame.qq.com",
    "build": "vue-cli-service build",
    "lint": "eslint --ext .js,.vue,.ts .",
    "lint:fix": "eslint --fix --ext .js,.vue,.ts .",
    "hippy:debug": "hippy-debug",
    "hippy:dev": "webpack --config ./scripts/hippy-webpack.dev.js",
    "hippy:vendor": "webpack --config ./scripts/hippy-webpack.ios-vendor.js --config ./scripts/hippy-webpack.android-vendor.js",
    "hippy:build": "webpack --config ./scripts/hippy-webpack.ios.js --config ./scripts/hippy-webpack.android.js",
    "hippy:publish": "hippy-publish igame-match-test",
    "hippy:publish:prod": "NODE_ENV='release' hippy-publish igame-match-release"
  },

可以看到,如果是sudo npm run dev的方式来启动服务的话,用的是vue-cli-service,这个是@vue/cli-service的命令,其默认是去读取根目录的配置文件vue.config.js。如果是以npm run hippy:dev 去启动的话,读取的是配置文件hippy-webpack.dev.js。由于两个配置文件不一样,所以,对ts的支持需要两边配置文件都要修改。

将现有js库改成ts库

| Comments

1. 背景

现在的公共库用js来书写,有两个问题:

  1. 调用方无法获得语法提示,且对调用函数的参数类型不明

  2. 公共库中的一些变量类型模糊不清

以上两个问题虽然不大,但是一定程度上影响了项目的开发效率和质量。通过typescript来改写公共库,有利于解决上述两个问题。

在现有iOS项目中集成Flutter方案

| Comments

1. 集成方式选择

官方提供了源码集成方式,详细见文章Add Flutter to existing apps

该方式有一个问题,Native工程和Flutter工程耦合性强,Native开发人员必须安装Flutter运行环境,才能运行真个工程,CI构建机上也要求有Flutter环境。更好的办法是将Flutter工程生成的产物集成进Native工程里,这样Native工程可以脱离Flutter环境运行。

__bridge __bridge retained __bridge transfer的区别

| Comments

iOS开发中,经常会接触到两种对象,Objective-C对象和Core Foundation对象,他们之间有所不同,可以互相转换。最大的不同之处在于,在ARC模式下,前者不用开发者手动管理内存,后者需要开发者手动管理内存,即调用CFRelease方法释放对象,否则会造成内存泄漏。转换的话主要会用到以下3个方法:

iOS 中的 NSProxy

| Comments

在日常开发中,NSObject 经常会被使用到。但是 NSProxy 却很少用。这个类顾名思义,是用来做代理的,任何消息都可以对它发送,在它内部,再指向具体的实现。

通过源码理解Autorelease Pool原理

| Comments

1. Autorelease Pool 是什么

iOS 的内存管理使用引用计数机制。当对象被初始化或者被强引用赋值时,对象的引用计数 +1,当对象离开所在函数作用域或者被设置为 nil 后,引用计数 -1。当对象的引用计数为 0 时,操作系统会释放掉对象所占用的内存。